在說Lambda表達式以前咱們瞭解一下函數式編程思想,在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是「拿什麼東西作什麼事情」。java
相對而言,面向對象過分強調「必須經過對象的形式來作事情」,而函數式思想則儘可能忽略面向對象的複雜語法——強調作什麼,而不是以什麼形式作。 下面以匿名內部類建立線程的代碼案例詳細說明這個問題。編程
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表達式數組
對以上代碼的分析:app
對於 Runnable 的匿名內部類用法,能夠分析出幾點內容:ide
Thread 類須要 Runnable 接口做爲參數,其中的抽象 run 方法是用來指定線程任務內容的核心;
爲了指定 run 的方法體,不得不須要 Runnable 接口的實現類;
爲了省去定義一個 RunnableImpl 實現類的麻煩,不得不使用匿名內部類;
必須覆蓋重寫抽象 run 方法,因此方法名稱、方法參數、方法返回值不得再也不寫一遍,且不能寫錯;
而實際上,彷佛只有方法體纔是關鍵所在。函數式編程
傳統的寫法比Lambda表達式寫法顯而易見代碼繁瑣了許多,並且2者實現目的是相同的。函數
咱們真的但願建立一個匿名內部類對象嗎?不。咱們只是爲了作這件事情而不得不建立一個對象。咱們真正但願作的事情是:將 run 方法體內的代碼傳遞給 Thread 類知曉。性能
傳遞一段代碼——這纔是咱們真正的目的。而建立對象只是受限於面向對象語法而不得不採起的一種手段方式。學習
那,有沒有更加簡單的辦法?若是咱們將關注點從「怎麼作」迴歸到「作什麼」的本質上,就會發現只要可以更好地達到目的,過程與形式其實並不重要。 ui
這時就要用到函數式編程思想了,只關注「作什麼」,而不是以什麼方式作!!
瞭解過函數式編程思想後,咱們要嘗試着轉變思想,從面向對象的"怎麼作"轉換爲函數式編程思想的「作什麼」,只有思想有了轉變,才能更好的瞭解和學習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的轉換,將一個類型的流轉換爲另外一個類型的流。