原文:http://blog.csdn.net/jinzhencs/article/details/50748202
lambda表達式詳解
一.問題
1.什么是lambda表達式?
2.lambda表達式用來干什么的?
3.lambda表達式的優缺點?
4.lambda表達式的使用場景?
5.lambda只是一個語法糖嗎?
二.概念
lambda表達式是JAVA8中提供的一種新的特性,它支持Java也能進行簡單的“函數式編程”。
它是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應於其中的lambda抽象(lambda abstraction),是一個匿名函數,即沒有函數名的函數。
三.先看看效果
先看幾個例子:
1.使用lambda表達式實現Runnable
package com.lambda; /** * 使用lambda表達式替換Runnable匿名內部類 * @author MingChenchen * */ public class RunableTest { /** * 普通的Runnable */ public static void runSomeThing(){ Runnable runnable = new Runnable() { @Override public void run() { System.out.println("I am running"); } }; new Thread(runnable).start(); } /** * 使用lambda后的 */ public static void runSomeThingByLambda(){ new Thread(() -> System.out.println("I am running")).start(); } public static void main(String[] args) { runSomeThing(); // runSomeThingByLambda(); } } 上述代碼中: () -> System.out.println("I am running")就是一個lambda表達式, 可以看出,它是替代了new Runnable(){}這個匿名內部類。
2.使用lambda表達式實現Comparator
package com.lambda; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; public class SortList { //給入一個List private static List<String> list = Arrays.asList("my","name","is","uber","and","uc"); /** * 對一個String的list進行排序 - 使用老方法 */ public static void oldSort(){ //排序 Collections.sort(list,new Comparator<String>() { //使用新的排序規則 根據第二個字符進行逆序排 @Override public int compare(String a,String b){ if (a.charAt(1) <= b.charAt(1)) { return 1; }else{ return -1; } } }); } /** * 新的排序方法 - 使用lambda表達式實現 */ public static void newSort(){ //lambda會自動推斷出 a,b 的類型 Collections.sort(list, (a, b) -> a.charAt(1) < b.charAt(1) ? 1:-1); } public static void main(String[] args) { // oldSort(); newSort(); } }
3.使用lambda表達式實現ActionListener
package com.lambda; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; public class ActionEventDemo { private JButton button = new JButton(); public void bindEvent(){ button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("你好!" ); } }); } /** * 使用Lambda表達式 為button添加ActionListener */ public void bindEventByLambda(){ button.addActionListener(e -> System.out.println("你好!")); } }
四.來由
好了,通過上述的幾個例子,大家差不多也能明白了lambda是用來干什么以及好處了。
顯而易見的,好處就是代碼量大大減少了!程序邏輯也很清晰明了。
它的用處淺顯來說就是替代“內部匿名類”、可以對集合或者數組進行循環操作。
以前:
面向對象式編程就應該純粹的面向對象,於是經常看到這樣的寫法:
如果你想寫一個方法,那么就必須把它放到一個類里面,然后new出來對象,對象調用這個方法。
匿名類型最大的問題就在於其冗余的語法。
有人戲稱匿名類型導致了“高度問題”(height problem):
比如大多匿名內部類的多行代碼中僅有一行在做實際工作。
因此JAVA8中就提供了這種“函數式編程”的方法 —— lambda表達式,供我們來更加簡明扼要的實現內部匿名類的功能。
五.什么時候可以使用它?
先說一個名詞的概念
函數式接口:Functional Interface.
定義的一個接口,接口里面必須 有且只有一個抽象方法 ,這樣的接口就成為函數式接口。
在可以使用lambda表達式的地方,方法聲明時必須包含一個函數式的接口。
(JAVA8的接口可以有多個default方法)
任何函數式接口都可以使用lambda表達式替換。
例如:ActionListener、Comparator、Runnable
lambda表達式只能出現在目標類型為函數式接口的上下文中。
注意:
此處是只能!!!
意味着如果我們提供的這個接口包含一個以上的Abstract Method,那么使用lambda表達式則會報錯。
這點已經驗證過了。
場景:
這種場景其實很常見:
你在某處就真的只需要一個能做一件事情的函數而已,連它叫什么名字都無關緊要。
Lambda 表達式就可以用來做這件事。
六.寫法、規則
基本語法:
(parameters) -> expression 或 (parameters) ->{ statements; }
即: 參數 -> 帶返回值的表達式/無返回值的陳述
//1. 接收2個int型整數,返回他們的和 (int x, int y) -> x + y; //2. 接受一個 string 對象,並在控制台打印,不返回任何值(看起來像是返回void) (String s) -> System.out.print(s);
七.幾個特性
1. 類型推導
編譯器負責推導lambda表達式的類型。它利用lambda表達式所在上下文所期待的類型進行推導,
這個被期待的類型被稱為目標類型。就是說我們傳入的參數可以無需寫類型了!
2.變量捕獲
在Java SE 7中,編譯器對內部類中引用的外部變量(即捕獲的變量)要求非常嚴格:
如果捕獲的變量沒有被聲明為final就會產生一個編譯錯誤。
我們現在放寬了這個限制——對於lambda表達式和內部類,
我們允許在其中捕獲那些符合有效只讀(Effectively final)的局部變量。
簡單的說,如果一個局部變量在初始化后從未被修改過,那么它就符合有效只讀的要求,
換句話說,加上final后也不會導致編譯錯誤的局部變量就是有效只讀變量。
注意:此處和final關鍵字一樣,指的是引用不可改!(感覺沒多大意義,還不是用的final)
3.方法引用
如果我們想要調用的方法擁有一個名字,我們就可以通過它的名字直接調用它。
Comparator byName = Comparator.comparing(Person::getName);
此處無需再傳入參數,lambda會自動裝配成Person類型進來然后執行getName()方法,而后返回getName()的String
方法引用有很多種,它們的語法如下:
靜態方法引用:ClassName::methodName
實例上的實例方法引用:instanceReference::methodName
超類上的實例方法引用:super::methodName
類型上的實例方法引用:ClassName::methodName
構造方法引用:Class::new
數組構造方法引用:TypeName[]::new
4.JAVA提供給我們的SAM接口
Java SE 8中增加了一個新的包:java.util.function,它里面包含了常用的函數式接口,例如:
Predicate<T>——接收T對象並返回boolean Consumer<T>——接收T對象,不返回值 Function<T, R>——接收T對象,返回R對象 Supplier<T>——提供T對象(例如工廠),不接收值 UnaryOperator<T>——接收T對象,返回T對象 BinaryOperator<T>——接收兩個T對象,返回T對象
那么在參數為這些接口的地方,我們就可以直接使用lambda表達式了!
八.更多的例子
1.自定義SAM接口,從而使用lambda表達式
package com.lambda.myaction; /** * 自定義一個函數式接口 * @author MingChenchen * */ public interface MyActionInterface { public void saySomeThing(String str); /** * Java8引入的新特性 接口中可以定義一個default方法的實現 (不是abstract) */ default void say(){ System.out.println("default say"); } }
package com.lambda.myaction; /** * 在我們自定義的函數式接口的地方使用lambda表達式 * @author MingChenchen * */ public class WantSay { /** * 執行接口中的saySomeThing方法 * @param action * @param thing */ public static void excuteSay(MyActionInterface action,String thing){ action.saySomeThing(thing); } public static void main(String[] args) { /* excuteSay(new MyActionInterface(){ @Override public void saySomeThing(String str) { System.out.println(str); } },"Hello World"); */ excuteSay((String s) -> System.out.println(s),"Hello world new"); } }
2.使用方法引用( ClassName::Method,無括號)
package com.lambda.usebean; /** * 實體類Person * @author MingChenchen * */ public class Person { private String name; //姓名 private String location; //地址 public String getName() { return name; } public void setName(String name) { this.name = name; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } @Override public String toString() { // TODO Auto-generated method stub return "Person:" + name + "," + location; } }
//使用String默認的排序規則,比較的是Person的name字段 Comparator<Person> byName = Comparator.comparing(p -> p.getName()); //不用寫傳入參數,傳入的用Person來聲明 Comparator<Person> byName2 = Comparator.comparing(Person::getName);
3.使用lambda表達式完成for-each循環操作
//原本的for-each循環寫做法 List list = Arrays.asList(....); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } //使用lambda表達式后的寫法 list.forEach(str -> System.out.println(str));
list.forEach()是JAVA8的新方法,支持函數式編程,此處使用的參數就是JAVA提供給我們的函數式接口:Consumer< T>
interface List<E> extends Collection<E> interface Collection<E> extends Iterable<E> public interface Iterable<T> { default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } }
4.一個完整的例子
//普通寫法: List<Person> people = ... Collections.sort(people, new Comparator<Person>() { public int compare(Person x, Person y) { return x.getLastName().compareTo(y.getLastName()); } }) //使用lambda表達式寫法: people.sort(comparing(Person::getLastName));
化簡流程:
第一步:去掉冗余的匿名類 Collections.sort(people,(Person x, Person y) -> x.getLastName().compareTo(y.getLastName())); 第二步:使用Comparator里的comparing方法 Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName())); 第三步:類型推導和靜態導入 Collections.sort(people, comparing(p -> p.getLastName())); 第四步:方法引用 Collections.sort(people, comparing(Person::getLastName)); 第五步:使用List本身的sort更優 people.sort(comparing(Person::getLastName));;
九.優缺點
優點:
1.極大的簡化代碼。去除了很多無用的Java代碼,使得代碼更為簡潔明了。
2.比匿名內部類更加高效(不確定)。編譯器會生成專門的lambda方法,可以使用javap -p查看編譯過后的代碼
缺點:
1.可讀性差。在代碼簡潔的情況下,另一方面又讓大多程序員很難讀懂。因為很少程序員接觸使用它。
(不過這個缺點不是本身缺點,而且源於程序員較少使用)
十.它是一個語法糖嗎?
答:
就我自身的理解來說,lambda表達式不算是一個語法糖。
語法糖就是說只是幫助我們程序員輕松的少寫一些代碼,之后編譯器幫我們把那部分代碼生成出來。
但是從編譯過后的結果來說,並不是自動幫我們生成一個內部匿名類,而是生成了一個lambda$X方法。
第二個就是lambda其實表達的是目前流行的“函數式編程”這種思維。區別於我們面向對象的思維方法。
這點我認為很有意義,即我們要從各種思維來對待事情。而不是說,面向對象的這種方法就是最NB的。
但是論壇基本都認為這是一個語法糖,也沒錯。畢竟它提倡的只是一種思想,而且jdk底層為lambda生成了新的高效的代碼這個事兒並不確定。
接下來介紹 lambda的 好哥們:stream.
stream的方法里面大多都使用了lambda表達式
stream概要
一.什么是stream?
官方解釋:
A sequence of elements supporting sequential and parallel aggregate operations.
簡單來講,stream就是JAVA8提供給我們的對於元素集合統一、快速、並行操作的一種方式。
它能充分運用多核的優勢,以及配合lambda表達式、鏈式結構對集合等進行許多有用的操作。
概念:
stream:可以支持順序和並行對元素操作的元素集合。
作用:
提供了一種操作大數據接口,讓數據操作更容易和更快
使用stream,我們能夠對collection的元素進行過濾、映射、排序、去重等許多操作。
中間方法和終點方法:
它具有過濾、映射以及減少遍歷數等方法,這些方法分兩種:中間方法和終端方法,
“流”抽象天生就該是持續的,中間方法永遠返回的是Stream,因此如果我們要獲取最終結果的話,
必須使用終點操作才能收集流產生的最終結果。區分這兩個方法是看他的返回值,
如果是Stream則是中間方法,否則是終點方法
二.如何使用stream?
1.通過Stream接口的靜態工廠方法(注意:Java8里接口可以帶靜態方法);
2.通過Collection接口的默認方法(默認方法:Default method,也是Java8中的一個新特性,就是接口中的一個帶有實現的方法)–stream(),把一個Collection對象轉換成Stream
一般情況下,我們都使用Collection接口的 .stream()方法得到stream.
三.常見的幾個中間方法
中間方法即是一些列對元素進行的操作。譬如過濾、去重、截斷等。
1.Filter(過濾)
//過濾18歲以上的人 List persons = … Stream personsOver18 = persons.stream().filter(p -> p.getAge() > 18);
2.Map(對元素進行操作)
//把person轉成Adult Stream map = persons.stream().filter(p -> p.getAge() > 18).map(person -> new Adult(person));
3.limit(截斷)
對一個Stream進行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數小於N,那就獲取其所有的元素
4.distinct(去重)
對於Stream中包含的元素進行去重操作(去重邏輯依賴元素的equals方法),新生成的Stream中沒有重復的元素
四.常用的終點方法
通過中間方法,我們對stream的元素進行了統一的操作,但是中間方法得到還是一個stream。要想把它轉換為新的集合、或者是統計等。我們需要使用終點方法。
1.count(統計)
count方法是一個流的終點方法,可使流的結果最終統計,返回int,比如我們計算一下滿足18歲的總人數
int countOfAdult=persons.stream() .filter(p -> p.getAge() > 18) .map(person -> new Adult(person)) .count();
2.Collect(收集流的結果)
collect方法也是一個流的終點方法,可收集最終的結果
List adultList= persons.stream() .filter(p -> p.getAge() > 18) .map(person -> new Adult(person)) .collect(Collectors.toList());
五.順序流和並行流
每個Stream都有兩種模式:順序執行和並行執行。
//順序流: List <Person> people = list.getStream.collect(Collectors.toList()); //並行流: List <Person> people = list.getStream.parallel().collect(Collectors.toList()); //可以看出,要使用並行流,只需要.parallel()即可
顧名思義,當使用順序方式去遍歷時,每個item讀完后再讀下一個item。
而使用並行去遍歷時,數組會被分成多個段,其中每一個都在不同的線程中處理,然后將結果一起輸出。
並行流原理:
List originalList = someData;
split1 = originalList(0, mid);//將數據分小部分
split2 = originalList(mid,end);
new Runnable(split1.process());//小部分執行操作
new Runnable(split2.process());
List revisedList = split1 + split2;//將結果合並
性能:如果是多核機器,理論上並行流則會比順序流快上一倍。
以下是借用他人的一個測試兩者性能的Demo.
package com.lambda.stream; import java.util.stream.IntStream; public class TestPerformance { public static void main(String[] args) { long t0 = System.nanoTime(); //初始化一個范圍100萬整數流,求能被2整除的數字,toArray()是終點方法 int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray(); long t1 = System.nanoTime(); //和上面功能一樣,這里是用並行流來計算 int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray(); long t2 = System.nanoTime(); //我本機的結果是serial: 0.06s, parallel 0.02s,證明並行流確實比順序流快 System.out.printf("serial: %.2fs, parallel %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9); } }
運行結果:
serial: 0.07s
parallel 0.02s
可以看出,並行流的效率確實提高了3.5倍(我本機是4核,電腦較差.)
進階學習:
1.Predicate和Consumer接口– Java 8中java.util.function包下的接口:
http://ifeve.com/predicate-and-consumer-interface-in-java-util-function-package-in-java-8/
2.深入理解Java 8 Lambda(語言篇——lambda,方法引用,目標類型和默認方法)
http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features/
參考資料:
1.Java8初體驗(二)Stream語法詳解:
http://ifeve.com/stream/
2.Java8初體驗(一)lambda表達式語法
http://ifeve.com/lambda/