Lambda表達式
在說Lambda表達式之前我們了解一下函數式編程思想,在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是“拿什么東西做什么事情”。
相對而言,面向對象過分強調“必須通過對象的形式來做事情”,而函數式思想則盡量忽略面向對象的復雜語法——強調做什么,而不是以什么形式做。 下面以匿名內部類創建線程的代碼案例詳細說明這個問題。
public class ThreadDemo {
public static void main(String[] args) {
//實現Runnable方式創建簡單線程--傳統匿名內部類形式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("開啟了一個線程----匿名內部類");
}
}).start();
//實現Runnable方式創建簡單線程--Lambda表達式形式
new Thread(()-> System.out.println("開啟了一個線程---Lambda表達式")).start();
}
}
運行結果:
開啟了一個線程----匿名內部類
開啟了一個線程---Lambda表達式
對以上代碼的分析:
對於 Runnable 的匿名內部類用法,可以分析出幾點內容:
Thread 類需要 Runnable 接口作為參數,其中的抽象 run 方法是用來指定線程任務內容的核心;
為了指定 run 的方法體,不得不需要 Runnable 接口的實現類;
為了省去定義一個 RunnableImpl 實現類的麻煩,不得不使用匿名內部類;
必須覆蓋重寫抽象 run 方法,所以方法名稱、方法參數、方法返回值不得不再寫一遍,且不能寫錯;
而實際上,似乎只有方法體才是關鍵所在。
傳統的寫法比Lambda表達式寫法顯而易見代碼繁瑣了許多,而且2者實現目的是相同的。
我們真的希望創建一個匿名內部類對象嗎?不。我們只是為了做這件事情而不得不創建一個對象。我們真正希望做的事情是:將 run 方法體內的代碼傳遞給 Thread 類知曉。
傳遞一段代碼——這才是我們真正的目的。而創建對象只是受限於面向對象語法而不得不采取的一種手段方式。
那,有沒有更加簡單的辦法?如果我們將關注點從“怎么做”回歸到“做什么”的本質上,就會發現只要能夠更好地達到目的,過程與形式其實並不重要。
這時就要用到函數式編程思想了,只關注“做什么”,而不是以什么方式做!!
了解過函數式編程思想后,我們要嘗試着轉變思想,從面向對象的"怎么做"轉換為函數式編程思想的“做什么”,只有思想有了轉變,才能更好的了解和學習Lambda表達式。
什么是Lambda表達式?
Lambda 是一個匿名函數,我們可以把 Lambda表達式理解為是一段可以傳遞的代碼(將代碼像數據一樣進行傳遞)。可以寫出更簡潔、更靈活的代碼。
作為一種更緊湊的代碼風格,使Java的語言表達能力得到了提升 。(2014年3月Oracle所發布的Java 8(JDK 1.8)中,加入了Lambda表達式 )
Lambda表達式語法:( ) -> { }
Lambda 表達式在Java 語言中引入了一個新的語法元素和操作符。這個操作符為 “->” , 該操作符被稱為 Lambda 操作符或剪頭操作符。它將 Lambda 分為
兩個部分:
左側 (): 指定了 Lambda 表達式需要的所有參數
右側 {}: 指定了 Lambda 體,即 Lambda 表達式要執行的功能。
Lambda表達式標准格式:(參數類型 參數名稱) ‐> { 代碼語句 }
格式進一步說明:
小括號內的語法與傳統方法參數列表一致:無參數則留空;多個參數則用逗號分隔。
-> 是新引入的語法格式,代表指向動作。
大括號內的語法與傳統方法體要求基本一致
Lambda表達式如何使用呢?
Lambda表達式的使用是有前提的,必須要滿足2個條件:1.函數式接口 2.可推導可省略。
函數式接口是指一個接口中只有一個必須被實現的方法。這樣的接口都滿足一個注解@FunctionalInterface
@FunctionalInterface public interface Runnable { public abstract void run(); }
可推導可省略是指上下文推斷,也就是方法的參數或局部變量類型必須為Lambda對應的接口類型,才能使用Lambda作為該接口的實例 。
下面我們自定義一個函數式接口,使用Lambda表達式完成功能。
public class Demo { public static void main(String[] args) { invokeCook(()->{ System.out.println("做了一盤紅燒魚...."); }); } //需要有個以函數式接口為參數的方法 public static void invokeCook(Cook cook) { cook.makeFood(); } } //自定義函數式接口 @FunctionalInterface interface Cook{ void makeFood(); }
以上案例是函數式接口以及Lambda表達式最簡單的定義和用法。
針對Lambda表達式還可以做出進一步的省略寫法:
1.小括號內參數的類型可以省略;
2. 如果小括號內有且僅有一個參,則小括號可以省略;
3. 如果大括號內有且僅有一個語句,則無論是否有返回值,都可以省略大括號、return關鍵字及語句分號。
所以上面的代碼可以簡寫為:
invokeCook(()-> System.out.println("做了一盤紅燒魚...."));
Lambda表達式有多種語法,下面我們了解一下。(直接寫省略形式)
1.無參,無返回值,Lambda體只需一條語句
Runnable r = ()->System.out.println("hell lambda");
2.Lambda表達式需要一個參數,無返回值
Consumer c = (str)-> System.out.println(args);
當Lambda表達式只有一個參數時,參數的小括號可以省略
Consumer c = str-> System.out.println(args);
3.Lambda表達式需要2個參數,並且有返回值
BinaryOperator<Long> bo = (num1,num2)->{ return num1+num2;};
當Lambda體中只有一條語句時,return 和 大括號、分號可以同時省略。
BinaryOperator<Long> bo = (num1,num2)-> num1+num2;
有沒有發現我們沒寫參數類型,Lambda表達式依然可以正確編譯和運行,這是因為Lambda表達式擁有的類型推斷功能。
上述 Lambda 表達式中的參數類型都是由編譯器推斷得出的。 Lambda 表達式中無需指定類型,程序依然可以編譯,這是因為 javac 根據程序的上下文,
在后台推斷出了參數的類型。 Lambda 表達式的類型依賴於上下文環境,是由編譯器推斷出來的。這就是所謂的“類型推斷” 。
Lambda表達式還具有延遲執行的作用:改善了性能浪費的問題,代碼說明。
public class Demo { public static void main(String[] args) { String str1 = "hello"; String str2 = "Lambda"; String str3 = "表達式"; log(1,str1+str2+str3); } public static void log(int level,String str) { if (level == 1) { System.out.println(str); } } }
在上面代碼中,存在的性能浪費問題是如果 輸入的level!=1,而str1+str2+str3作為log方法的第二個參數還是參與了拼接運算,但是我們的實際想法應該是不滿足level=1的條件就不希望str1+str2+str3進行拼接運算,下面通過Lambda表達式來實現這個功能。
public class Demo { public static void main(String[] args) { String str1 = "hello"; String str2 = "Lambda"; String str3 = "表達式"; log(1,()->str1+str2+str3); } public static void log(int level,Message message) { if (level == 1) { System.out.println(message.message()); } } } @FunctionalInterface interface Message { String message(); }
以上代碼功能相同,Lambda表達式卻實現了延遲,解決了性能浪費,下面我們來驗證一下:
public class Demo { public static void main(String[] args) { String str1 = "hello"; String str2 = "Lambda"; String str3 = "表達式"; log(2,()->{ System.out.println("lambda 執行了"); return str1+str2+str3; }); } public static void log(int level,Message message) { if (level == 1) { System.out.println(message.message()); } } } @FunctionalInterface interface Message { String message(); }
此時在輸入level=2的條件時,如果Lambda不延遲加載的話會執行輸出語句輸出lambda 執行了,而實際是控制台什么也沒輸出,由此驗證了Lambda表達式的延遲執行。
在Lambda表達式的應用過程中還有一種比較常用的方式:方法引用。方法引用比較難以理解,而且種類也較多,需要多費腦筋去理解。
Lambda表達式應用之 :方法引用
方法引用也是有前提的,分別為:
1.前后的參數名一致,
2.Lambda表達式的方法體跟對應的方法的功能代碼要一模一樣
方法引用種類可以簡單的分為4+2種,4種跟對象和類有關,2種跟構造方法有關。下面一一說明。
跟對象和類有關的方法引用:
1.對象引用成員方法
格式:對象名 :: 成員方法名 (雙冒號 :: 為引用運算符,而它所在的表達式被稱為方法引用)
原理:將對象的成員方法的參數和方法體,自動生成一個Lambda表達式。
1 public class Demo { 2 public static void main(String[] args) { 3 Assistant assistant = new Assistant(); 4 work(assistant::dealFile);//對象引用成員方法(注意是成員的方法名,沒有小括號) 5 } 6 //以函數式接口為參數的方法 7 public static void work(WokerHelper wokerHelper) { 8 wokerHelper.help("機密文件"); 9 } 10 } 11 //助理類,有個成員方法 12 class Assistant{ 13 public void dealFile(String file) { 14 System.out.println("幫忙處理文件:"+file); 15 } 16 } 17 //函數式接口,有個需要實現的抽象方法 18 @FunctionalInterface 19 interface WokerHelper { 20 void help(String file); 21 }
2.類調用靜態方法
格式:類名 :: 靜態方法名
原理:將類的靜態方法的參數和方法體,自動生成一個Lambda表達式。
public class Demo { public static void main(String[] args) { methodCheck((str)->StringUtils.isBlank(str)," ");//非省略模式 methodCheck(StringUtils::isBlank," ");//省略模式 類名調用靜態方法 } // public static void methodCheck(StringChecker stringChecker,String str) { System.out.println(stringChecker.checkString(str)); } } //定義一個類包含靜態方法isBlank方法 class StringUtils{ public static boolean isBlank(String str) { return str==null || "".equals(str.trim());//空格也算空 } } //函數式接口,有個需要實現的抽象方法 @FunctionalInterface interface StringChecker { boolean checkString(String str); }
3.this引用本類方法
格式:this :: 本類方法名
原理:將本類方法的參數和方法體,自動生成一個Lambda表達式。
public class Demo { public static void main(String[] args) { new Husband().beHappy(); } } class Husband{ public void buyHouse() { System.out.println("買套房子"); } public void marry(Richable richable) { richable.buy(); } public void beHappy() { marry(this::buyHouse);//調用本類中方法 } } //函數式接口,有個需要實現的抽象方法 @FunctionalInterface interface Richable { void buy(); }
4.super引用父類方法
格式:super :: 父類方法名
原理:將父類方法的參數和方法體,自動生成一個Lambda表達式。
public class Demo { public static void main(String[] args) { new Man().sayHello(); } } //子類 class Man extends Human{ public void method(Greetable greetable) { greetable.greet(); } @Override public void sayHello() { method(super::sayHello); } } //父類 class Human{ public void sayHello() { System.out.println("Hello"); } } //函數式接口,有個需要實現的抽象方法 @FunctionalInterface interface Greetable { void greet(); }
跟構造方法有關的方法引用:
5.類的構造器引用
格式: 類名 :: new
原理:將類的構造方法的參數和方法體自動生成Lambda表達式。
public class Demo { public static void main(String[] args) { printName("張三",(name)->new Person(name)); printName("張三",Person::new);//省略形式,類名::new引用 } public static void printName(String name, BuildPerson build) { System.out.println(build.personBuild(name).getName()); } } //函數式接口,有個需要實現的抽象方法 @FunctionalInterface interface BuildPerson { Person personBuild(String name); } //實體類 class Person{ String name; public Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } }
6.數組的構造器引用
格式: 數組類型[] :: new
原理:將數組的構造方法的參數和方法體自動生成Lambda表達式。
public class Demo { public static void main(String[] args) { int[] array1 = method(10, (length) -> new int[length]); int[] array2 = method(10, int[]::new);//數組構造器引用 } public static int[] method(int length, ArrayBuilder builder) { return builder.buildArray(length); } } //函數式接口,有個需要實現的抽象方法 @FunctionalInterface interface ArrayBuilder { int[] buildArray(int length); }
到此,Lambda表達式的基本知識就算學完了。
有人可能會提出疑問,Lambda表達式使用前要定義一個函數式接口,並在接口中有抽象方法,還要創建一個以函數式接口為參數的方法,之后調用該方法才能使用Lambda表達式,感覺並沒有省很多代碼!!哈哈,之所以有這樣的想法,那是因為是我們自定義的函數式接口,而JDK1.8及更高的版本都給我們定義函數式接口供我們直接使用,就沒有這么繁瑣了。接下來我們學習一下JDK為我們提供的常用函數式接口。
常用的函數式接口
1.Supplier<T> 供給型接口
@FunctionalInterface public interface Supplier<T> { T get(); }
用來獲取一個泛型參數指定類型的對象數據。由於這是一個函數式接口,這也就意味着對應的Lambda表達式需要“對外提供”一個符合泛型類型的對象數據。
如果要定義一個無參的有Object返回值的抽象方法的接口時,可以直接使用Supplier<T>,不用自己定義接口了。
public class Demo { public static void main(String[] args) { String str1 = "hello"; String str2 = "lambda"; String s = method(() -> str1 + str2); System.out.println("s = " + s); } public static String method(Supplier<String> supplier) { return supplier.get(); } }
2.Consumer<T> 消費型接口
@FunctionalInterface public interface Consumer<T> {
void accept(T t);
//合並2個消費者生成一個新的消費者,先執行第一個消費者的accept方法,再執行第二個消費者的accept方法 default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
Consumer<T> 接口則正好相反,它不是生產一個數據,而是消費一個數據,其數據類型由泛型參數決定 。
如果要定義一個有參的無返回值的抽象方法的接口時,可以直接使用Consumer<T>,不用自己定義接口了。
public class Demo { public static void main(String[] args) { consumerString(string -> System.out.println(string)); consumerString(System.out::println);//方法引用形式 } public static void consumerString(Consumer<String> consumer) { consumer.accept("fall in love!"); } }
3.Predicate<T> 斷定型接口
@FunctionalInterface
public interface Predicate<T> {
//用來判斷傳入的T類型的參數是否滿足篩選條件,滿足>true
boolean test(T t);
//合並2個predicate成為一個新的predicate---->並且&&
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
//對調用的predicate原來的結果進行取反---->取反 !
default Predicate<T> negate() {
return (t) -> !test(t);
}
//合並2個predicate成為一個新的predicate---->或||
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
}
Predicate<T>接口主要是對某種類型的數據進行判斷,返回一個boolean型結果。可以理解成用來對數據進行篩選。
當需要定義一個有參並且返回值是boolean型的方法時,可以直接使用Predicate接口中的抽象方法
1 //1.必須為女生; 2 //2. 姓名為4個字。 3 public class Demo { 4 public static void main(String[] args) { 5 String[] array = { "迪麗熱巴,女", "古力娜扎,女", "馬爾扎哈,男", "趙麗穎,女" }; 6 List<String> list = filter(array, 7 str-> "女".equals(str.split(",")[1]), 8 str->str.split(",")[0].length()==3); 9 System.out.println(list); 10 } 11 private static List<String> filter(String[] array, Predicate<String> one, Predicate<String> two) { 12 List<String> list = new ArrayList<>(); 13 for (String info : array) { 14 if (one.and(two).test(info)) { 15 list.add(info); 16 } 17 } 18 return list; 19 } 20 }
4.Function<T,R> 函數型接口
@FunctionalInterface public interface Function<T, R> { //表示數據轉換的實現。T--->R R apply(T t); //合並2個function,生成一個新的function,調用apply方法的時候,先執行before,再執行this default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } //合並2個function,生成一個新的function,調用apply方法的時候,先執行this,再執行after default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } }
Function<T,R> 接口用來根據一個類型的數據得到另一個類型的數據,前者稱為前置條件,后者稱為后置條件。有進有出,所以稱為“函數Function”。
該接口可以理解成一個數據工廠,用來進行數據轉換,將一種數據類型的數據轉換成另一種數據. 泛型參數T:要被轉換的數據類型(原料),泛型參數R:想要裝換成的數據類型(產品)。
public class Demo { public static void main(String[] args) { String str = "趙麗穎,20"; int age = getAgeNum(str, string ->string.split(",")[1], Integer::parseInt,//str->Integer.parseInt(str); n->n+=100); System.out.println(age); } //實現三個數據轉換 String->String, String->Integer,Integer->Integer private static int getAgeNum(String str, Function<String, String> one, Function<String, Integer> two, Function<Integer, Integer> three) { return one.andThen(two).andThen(three).apply(str); } }
至此,常用的四個函數式接口已學習完畢。
總結一下函數式表達式的延遲方法與終結方法:
延遲方法:默認方法都是延遲的。
終結方法:抽象方法都是終結的。
接口名稱 | 方法名稱 | 抽象方法/默認方法 | 延遲/終結 |
Supplier | get | 抽象 | 終結 |
Consumer | accept | 抽象 | 終結 |
andThen | 默認 | 延遲 | |
Predicate | test | 抽象 | 終結 |
and | 默認 | 延遲 | |
or | 默認 | 延遲 | |
negate | 默認 | 延遲 | |
Function | apply | 抽象 | 終結 |
andThen | 默認 | 延遲 |
函數式接口在Stream流中的應用較為廣泛,其中Stream流中的過濾Filter方法使用到了Predicate的判定,map方法使用到了Function的轉換,將一個類型的流轉換為另一個類型的流。