java8 lambda 表達式詳解


  • lambada 表達式實質上是一個匿名方法,但該方法並非獨立執行,而是用於實現由函數式接口定義的唯一抽象方法
  • 使用 lambda 表達式時,會創建實現了函數式接口的一個匿名類實例
  • 可以將 lambda 表達式視為一個對象,可以將其作為參數傳遞

1. 函數式接口

函數式接口是僅含一個抽象方法的接口,但可以指定 Object 定義的任何公有方法。

  • 以下是一個函數式接口:
1 @FunctionalInterface
2 public interface IFuntionSum<T extends Number> {
3     T sum(List<T> numbers);      // 抽象方法
4 }

 

  • 以下也是一個函數式接口:
1 @FunctionalInterface
2 public interface IFunctionMulti<T extends Number> {
3     void multi(List<T> numbers); // 抽象方法
4     
5     boolean equals(Object obj);  // Object中的方法
6 }

 

  • 但如果改為以下形式,則不是函數式接口:
1 @FunctionalInterface
2 public interface IFunctionMulti<T extends Number> extends IFuntionSum<T> {
3     void multi(List<T> numbers);
4     
5     @Override
6     boolean equals(Object obj);
7 }
8 // IFunctionMulti 接口繼承了 IFuntionSum 接口,此時 IFunctionMulti 包含了2個抽象方法

 

tip 1: 可以用 @FunctionalInterface 標識函數式接口,非強制要求,但有助於編譯器及時檢查接口是否滿足函數式接口定義
tip 2: 在 Java 8 之前,接口的所有方法都是抽象方法,在 Java 8 中新增了接口的默認方法

2. lambda 表達式

  • lambda 表達式的2種形式
    包含單獨表達式 :parameters -> an expression

     1 list.forEach(item -> System.out.println(item)); 
    

    包含代碼塊:parameters -> { expressions };

    list.forEach(item -> {
      int numA = item.getNumA();
      int numB = item.getNumB();
          System.out.println(numA + numB);
    });

     

    左側指定 lambda 表達式需要的參數,右側指定 lambda 方法體

  • 上文提到,lambda 無法獨立執行,它必須是實現一個函數式接口的唯一抽象方法。
    每個 lambda 表達式背后必定有一個函數式接口,該表達式實現的是這個函數式接口內部的唯一抽象方法。

    譬如以下 lambda 表達式:

    list.forEach(item -> System.out.println(item));

     

    我們看 ArrayList 中 foreach 方法:

    @Override
    public void forEach(Consumer<? super E> action) {
      // 太長了,不看了~
    }

     

    其中 Consumer 是一個函數式接口:

    @FunctionalInterface
    public interface Consumer<T> {
        void accept(T t);    // lambda 表達式 item -> System.out.println(item) 實現了該方法
    
        default Consumer<T> andThen(Consumer<? super T> after) {
            Objects.requireNonNull(after);
            return (T t) -> { accept(t); after.accept(t); };
        }
    }

     

    Consumer 接口是 Java 8 中預先定義的函數式接口,java.util.function 包下都是些預定義的函數式接口
    function 包下的部分接口使用了泛型,具有很強的通用性,在自定義函數式接口前,不妨去這個包下找找有沒有能用的

  • 在執行 lambda 表達式時,會自動創建一個實現了目標函數式接口的類實例,該類實例是一個匿名內部類。

    同樣以 list 的 foreach 方法為例:

    @FunctionalInterface
    public interface Consumer<T> {
        void accept(T t);    // lambda 表達式 item -> System.out.println(item) 實現了該方法
    
        default Consumer<T> andThen(Consumer<? super T> after) {
            Objects.requireNonNull(after);
            return (T t) -> { accept(t); after.accept(t); };
        }
    }

     

    用 Java VisualVM 追蹤代碼運行過程中的堆內存,發現會生成以下實例:

    生成的實例類名為 LambdaDemo$$Lambda$1,根據匿名內部類的命名規則可知,這是 LamddaDemo的 一個匿名內部類。(普通匿名內部類 和 lambda匿名內部類的命名規則見下文)。
  • 同樣,由於 lambda 表達式在執行時會生成目標函數式接口的類實例,因此我們可以做以下操作:

    // 有以下函數式接口
    @FunctionalInterface
    public interface IFuntionSum<T extends Number> {
        T sum(List<T> numbers);
    }
    
    // 將一個lambda表達式賦值給函數式接口引用(類型須兼容)
    IFuntionSum<Long> function = list -> {
      Long sum = 0L;
      for (Long item : list) {
          sum += item;
      }
      return sum;
    };
    function.sum(Arrays.asList(1L, 2L));   // 執行結果為3L

     

    在開發過程中,我們可以將 lambda 表達式等同於一個對象使用,對其聲明、引用、傳遞。

  • 匿名內部類 和 lambda 表達式匿名內部類的命名規則

    內部類的命名規則:外部類名 + $ + 內部類名
    匿名類的命名規則:外部類名 + $ + (1, 2, 3,第幾個匿名類就顯示幾)
    lambada 匿名內部類的命名規則:外部類名 + $$ + Lambda + $ + (1, 2, 3,第幾個lambda表達式就顯示幾)

    假設外部類中用到了2個lambda 表達式,則生成的2個匿名內部類的命名分別為 :

    外部類名$$Lambda$1 和 外部類名$$Lambda$2 

3. lambda 表達式規約

  • lambda 表達式的參數可以通過上下文推斷,如果需要顯示聲明一個參數的類型,則必須為所有的參數聲明類型。

    @FunctionalInterface
    public interface IFunctionMod {
      boolean (int n, int d);
    }
      
    IFunctionMod function = (n, d) -> (n % d) == 0          // 合理,n 和 d 的類型通過上下文推斷
    IFunctionMod function = (int n, int d) -> (n % d) == 0   // 合理,指定 n 和 d 的類型
    IFunctionMod function = (int n, d) -> (n % d) == 0       // 不合理,須顯示聲明所有參數類型

     

  • lambda 表達式中拋出的異常需要與目標函數式接口的抽象方法拋出的異常類型兼容:
    以下是合理的:

    @FunctionalInterface
    public interface IFunctionMod {
      boolean (int n, int d) throw Exception; 
    }
    
    IFunctionMod function = (n, d) -> {
        if (d == 0) {
          // IOException是EXception 的子類,通過類型轉換,IOException 可轉換為 Exception
          throw new IOException("test"); 
        } 
        return n % d == 0;
    };

     

    如果反一下,就不行了:

    @FunctionalInterface
    public interface IFunctionMod {
      boolean (int n, int d) throw IOException; 
    }
    
    IFunctionMod function = (n, d) -> {
        if (d == 0) {
          // 父類不能通過自動類型轉換轉為子類,lambda 表達式拋出的異常類型與抽象方法拋出的異常類型不兼容
          throw new Exception("test");
        } 
        return n % d == 0;
    };

     

  • lambda 表達式中參數類型需要與目標函數式接口中抽象方法的參數類型兼容。

    tip :從接口與實現的角度,可以很容易理解拋出異常兼容 和 參數類型兼容 這2點。

4. 方法引用

可以引用已有方法構造 lambda 表達式,這里給一個例子,不做詳細解釋:

list.forEach(System.out::print)

原文作者:EricAlpha:https://www.jianshu.com/p/613a6118e2e0

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM