JAVA8之lambda表達式詳解

 

原文:http://blog.csdn.net/jinzhencs/article/details/50748202html

 

lambda表達式詳解


一.問題

1.什麼是lambda表達式? 
2.lambda表達式用來幹什麼的? 
3.lambda表達式的優缺點? 
4.lambda表達式的使用場景? 
5.lambda只是一個語法糖嗎?java


二.概念

lambda表達式是JAVA8中提供的一種新的特性,它支持Java也能進行簡單的「函數式編程」。 
它是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應於其中的lambda抽象(lambda abstraction),是一個匿名函數,即沒有函數名的函數。程序員


三.先看看效果

先看幾個例子: 
1.使用lambda表達式實現Runnableexpress

複製代碼
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是用來幹什麼以及好處了。 
顯而易見的,好處就是代碼量大大減小了!程序邏輯也很清晰明瞭。 
它的用處淺顯來講就是替代「內部匿名類」、能夠對集合或者數組進行循環操做。ide

之前: 
面向對象式編程就應該純粹的面向對象,因而常常看到這樣的寫法: 
若是你想寫一個方法,那麼就必須把它放到一個類裏面,而後new出來對象,對象調用這個方法。 
匿名類型最大的問題就在於其冗餘的語法。 
有人戲稱匿名類型致使了「高度問題」(height problem): 
好比大多匿名內部類的多行代碼中僅有一行在作實際工做。函數式編程

所以JAVA8中就提供了這種「函數式編程」的方法 —— lambda表達式,供咱們來更加簡明扼要的實現內部匿名類的功能。函數


五.何時可使用它?

先說一個名詞的概念oop

函數式接口: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提供給咱們的對於元素集合統1、快速、並行操做的一種方式。 
它能充分運用多核的優點,以及配合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/

相關文章
相關標籤/搜索