天天看點

java8 lambda表達式的簡單介紹Lambda表達式的簡單介紹

Lambda表達式的簡單介紹

前言

   ​ Java 是一流的面向對象語言,除了部分簡單資料類型,Java 中的一切都是對象,即使數組也是一種對象,每個類建立的執行個體也是對象。在 Java 中定義的函數或方法不可能完全獨立,也不能将方法作為參數或傳回一個方法給執行個體。

​   ​ 我們總是通過匿名類給方法傳遞函數功能,以下是舊版的事件監聽代碼

someObject.addMouseListener(new MouseAdapter() {
  public void mouseClicked(MouseEvent e) {
    //Event listener implementation goes here...
  }
});
           

   ​ ​為了給 Mouse 監聽器添加自定義代碼,我們定義了一個匿名内部類 MouseAdapter 并建立了它的對象,通過這種方式,我們将一些函數功能傳給 addMouseListener 方法。

​   ​ 匿名類型最大的問題就在于其備援的文法。有人戲稱匿名類型導緻了“高度問題”(height problem):比如前面

MouseAdapter

的例子裡的五行代碼中僅有一行在做實際工作。

為什麼 Java 需要 Lambda 表達式?

​   ​ 在函數式語言中,我們隻需要給函數配置設定變量,并将這個函數作為參數傳遞給其它函數就可實作特定的功能。而java如前言中所述,不能直接将方法當作一個參數傳遞。同時匿名内部類又存在諸多不便:文法過于備援,匿名類中的

this

和變量名容易使人産生誤解,類型載入和執行個體建立語義不夠靈活,無法捕獲非

final

的局部變量等。

   ​ Lambda 表達式的出現為 Java 添加了缺失的函數式程式設計特點,使我們能将函數當做一等公民看待。

先說說函數式接口

​   ​ 我們将隻包含一個抽象方法聲明的接口稱為函數式接口。(之前它們被稱為SAM類型,即單抽象方法類型(Single Abstract Method))。Java SE7中就已存在函數式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.beans.PropertyChangeListener
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
           

​   ​ @FunctionalInterface注解來顯式指定一個接口是函數式接口(以避免無意聲明了一個符合函數式标準的接口),加上這個注解之後,編譯器就會驗證該接口是否滿足函數式接口的要求。Java SE 8中增加了一個新的包

java.util.function

,它裡面包含了很多常用的函數式接口。

   ​ 下面是stream流中常用到的一些新增的函數式接口:

@FunctionalInterface
public interface Function<T, R> {  
    R apply(T t);
}

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
           

Lambda 表達式

​   ​ Lambda 表達式是一種匿名函數(雖然并不完全正确),簡單地說,它是沒有聲明的方法,也即沒有通路修飾符、傳回值聲明和名字。它提供了輕量級的文法,進而解決了匿名内部類帶來的“高度問題”。

   ​ Java 中的 Lambda 表達式通常使用

(argument) -> (body)

文法書寫,例如:

(arg1, arg2...) -> { body }

(type1 arg1, type2 arg2...) -> { body }
           

​   ​ 以下是一些 Lambda 表達式的例子:

(int a, int b) -> {  return a + b; }

() -> System.out.println("Hello World");

(String s) -> { System.out.println(s); }

() -> 

() -> { return  };
           
  • 一個 Lambda 表達式可以有零個或多個參數
  • 參數的類型既可以明确聲明,也可以根據上下文來推斷。例如:

    (int a)

    (a)

    效果相同
  • 所有參數需包含在圓括号内,參數之間用逗号相隔。例如:

    (a, b)

    (int a, int b)

    (String a, int b, float c)

  • 空圓括号代表參數集為空。例如:

    () -> 42

  • 當隻有一個參數,且其類型可推導時,圓括号()可省略。例如:

    a -> return a*a

  • Lambda 表達式的主體可包含零條或多條語句
  • 如果 Lambda 表達式的主體隻有一條語句,花括号{}可省略。匿名函數的傳回類型與該主體表達式一緻
  • 如果 Lambda 表達式的主體包含一條以上語句,則表達式必須包含在花括号{}中(形成代碼塊)。匿名函數的

    傳回類型與代碼塊的傳回類型一緻,若沒有傳回則為空

目标類型?

​   ​ 函數式接口的名稱并不是lambda表達式的一部分。那麼對于給定的lambda表達式,它的類型是什麼?答案是:它的類型是由其上下文推導而來。

   ​ 這就意味着同樣的lambda表達式在不同上下文裡可以擁有不同的類型:

FileFilter f = (t) -> true;

Predicate p = (t) -> true;
           

   ​ 第一個lambda表達式

(t) -> true

FileFilter

的執行個體,而第二個lambda表達式則是

Predicate

的執行個體。

​   ​ 編譯器負責推導lambda表達式的類型。它利用lambda表達式所在上下文所期待的類型進行推導,這個被期待的類型被稱為目标類型。lambda表達式隻能出現在目标類型為函數式接口的上下文中。

來看幾個lambda例子

1.線程初始化 and 事件處理

//線程初始化
//舊方法:
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello from thread");
    }
}).start();

//新方法:
new Thread(() -> System.out.println("Hello from thread")).start();

//1.2事件處理
//舊方法:
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("The button was clicked using old fashion code!");
    }
});

//新方法:
button.addActionListener( (e) -> {
  System.out.println("The button was clicked. From Lambda expressions !");
});
           

2.數組列印

// forEach
//舊方法:
List<Integer> list = Arrays.asList(, , , , , , );
for(Integer n: list) {
   System.out.println(n);
}

//新方法:
List<Integer> list = Arrays.asList(, , , , , , );
list.forEach(n -> System.out.println(n));


//使用方法引用
//使用 Java 8 全新的雙冒号(::)操作符将一個正常方法轉化為 Lambda 表達式
list.forEach(System.out::println);
           

3.stream流相關使用

//3.1 map
//舊方法:
List<Integer> list = Arrays.asList(,,,,,,);
for(Integer n : list) {
    int x = n * n;
    System.out.println(x);
}

//新方法:
List<Integer> list = Arrays.asList(,,,,,,);
list.stream().map((x) -> x*x).forEach(System.out::println);

//3.2 filter
//3.3 reduce
           

方法引用

​   ​ 有時候,我們的Lambda表達式可能僅僅調用一個已存在的方法,而不做任何其它事,對于這種情況,通過一個方法名字來引用這個已存在的方法會更加清晰,Java 8的方法引用允許我們這樣做。方法引用是一個更加緊湊,易讀的Lambda表達式,注意方法引用是一個Lambda表達式,其中方法引用的操作符是雙冒号”::”。

​ 例如:

(n) -> System.out.println(n)

System.out::println
           

​   ​ 方法引用分為4類,常用的是前兩種。方法引用也受到通路控制權限的限制,可以通過在引用位置是否能夠調用被引用方法來判斷。具體分類資訊如下:

  • 引用靜态方法

    ContainingClass::staticMethodName

    例子: String::valueOf,對應的Lambda:(s) -> String.valueOf(s)

    比較容易了解,和靜态方法調用相比,隻是把.換為::

  • 引用特定對象的執行個體方法

    containingObject::instanceMethodName

    例子: x::toString,對應的Lambda:() -> this.toString()

    與引用靜态方法相比,都換為執行個體的而已

  • 引用特定類型的任意對象的執行個體方法

    ContainingType::methodName

    例子: String::toString,對應的Lambda:(s) -> s.toString()

    太難以了解了。難以了解的東西,也難以維護。建議還是不要用該種方法引用。

    執行個體方法要通過對象來調用,方法引用對應Lambda,Lambda的第一個參數會成為調用執行個體方法的對象。

  • 引用構造函數

    ClassName::new

    例子: String::new,對應的Lambda:() -> new String()

    構造函數本質上是靜态方法,隻是方法名字比較特殊。

如何處理異常?

​   ​ 如果函數接口的方法本身沒有定義可以被抛出的受檢異常,那麼在使用該接口時是無法處理可能存在的受檢異常的,比如典型的IOException這類:

public static void main(String[] args) {
        new Thread(() -> {
            // 會提示有未處理異常
            throw new Exception();
        }).start();
}
           

​   ​ 我們有兩個選擇:

​   ​ 1.在Lambda表達式内處理受檢異常

​   ​ 2.捕獲該受檢異常并重新以非受檢異常(如RuntimeException)的形式抛出

public void notThrowExce() {
        new Thread(() -> {
            try {
                throw new Exception();
            } catch (Exception e) {
                //  内部處理

                //  抛出不受檢異常
                throw new RuntimeException();
            }
        }).start();
}
           

​   ​ 如果函數接口的方法本身定義了可以被抛出的受檢異常

@FunctionalInterface
public interface WorkerInterface {
    void doSomeWork() throws Exception;
}

public class Worker implements WorkerInterface{
    private WorkerInterface workerInterface;

    public Worker(WorkerInterface workerInterface) {
        this.workerInterface = workerInterface;
    }

    @Override
    public void doSomeWork() throws Exception {
        workerInterface.doSomeWork();
    }
}
           
public void throwExce() throws Exception {
        // 可以将異常抛出
        new Worker(() -> { throw new Exception(); }).doSomeWork();
}
           

Lambda 表達式與匿名類的差別

​   ​ 1.對于匿名類,關鍵詞

this

解讀為匿名類,而對于 Lambda 表達式,關鍵詞

this

解讀為寫就 Lambda 的外部類。

   ​ 2.Java 編譯器編譯 Lambda 表達式時将他們轉化為類裡面的私有函數