java8學習:lambda表示式(2)

期待l發表於2018-11-07

內容來自《 java8實戰 》,本篇文章內容均為非盈利,旨為方便自己查詢、總結備份、開源分享。如有侵權請告知,馬上刪除。
書籍購買地址:java8實戰

  • 緊接上一篇內容,上一篇內容講了lambda的使用,自定義函式式介面和它自帶的函式式介面,這一篇將講述lambda的型別檢查,方法引用和構造器引用等內容
  • 上一篇內容提到過,lambda表示式可以為函式式介面生成一個例項,類似匿名內部類的功能,但是lambda本身並不包含他在實現哪個函式式介面的資訊
  • 下面來看一下lambda的型別檢查

    • 上一篇文章中說到的,lambda的傳入引數可寫可不寫,如下
    Predicate<Integer> predicate = (i) -> i == 3;  
    • 那麼她是怎麼知道i值是Integer型別的呢? 為了說明這個情況,我們可以參考下面程式碼
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Apple {
        private String color;
        private Integer weight;
    }
    public class Java8 {
        @Test
        public void test() throws Exception {
            List<Apple> apples = Arrays.asList();
            filterApple(apples,apple -> apple.getWeight() > 100);
        }
        private void filterApple(List<Apple> apples, Predicate<Apple> predicate){
            for (Apple apple : apples) {
                if (predicate.test(apple)){
                    System.out.println(apple);
                }
            }
        }
    }
    • test方法中的lambda並沒有標註apple變數是Apple型別的,那麼他為了知道apple變數的型別他會這樣做

      • 首先檢視filterApple方法簽名,從中我們可以看到目標型別是Predicate,這樣T泛型就繫結到了Apple
      • 然後檢視Predicate內的抽象方法,他接受一個Apple。返回一個boolean
      • 這樣函式描述符和lambda的簽名是一樣的都是傳入Apple,apple.getWeight() > 100會返回boolean,所以型別檢查是無誤的
  • 我們來看下面這個值得注意的地方

    Predicate<Integer> predicate = integer -> list.add(integer);
    • 上面的表示式很正常並沒有需要值得注意的地方,list.add方法本來就是返回Predicate抽象方法定義的返回值boolean
    Consumer<Integer> consumer = integer -> list.add(integer);
    • 上面的就會有點疑問了,但是它確實是正確的,Consumer的定義為T->{},所以這就會有一個void相容規則:如果lambda的主體是一個語句表示式,他就和一個返回void的函式描述符相容,如上面其實是返回Boolean,而且Consumer明確的說明返回void,他們肯定是不相容會報錯的,但是這個規則就會讓他們和平相處
  • 型別推斷

    • 上面以及講到了lambda是怎麼推斷出引數的型別的,那麼我們其實就不用費勁的明確給出其引數的型別,比如
    Comparator<Integer> comparator = (Integer a1,Integer a2) -> a1.compareTo(a2);
    • 完全可以寫成
    Comparator<Integer> comparator = (a1,a2) -> a1.compareTo(a2);   //以後還可以簡化程式碼
  • 使用區域性變數的限制

    public class Java8 {
        private static int a = 0;
        private int b = 1;
        @Test
        public void test() throws Exception {
            int c = 2;
            IntConsumer consumera = (num) -> System.out.println(num + a);
            IntConsumer consumerb = (num) -> System.out.println(num + b);
            IntConsumer consumerc = (num) -> System.out.println(num + c);
            a = 10;
            b = 11;
            //c = 12;  //只要是重新賦值那麼上面引用c變數的地方就會出錯
        }
    }
    • 如上我們總結出了,類中的類變數和例項變數,lambda引用他們對他們並不造成影響,但是引用區域性變數c的話,c就會被隱式的賦予final修飾
    • 對區域性限制的原因

      • 首先是不鼓勵使用這種能夠改變外部變數的典型指令式程式設計
      • 並且例項變數和區域性變數的實現是不一樣的,例項變數是儲存在堆中的,但是區域性變數是儲存在棧中,隨著方法彈棧就消失了,考慮以下場景:主執行緒中定義了c,但是他又開啟了一個執行緒B,線上程B中的lambda訪問A中的c,這時候可能會在A把c回收以後B再去訪問,因此java在訪問自由區域性變數時,實際上是在訪問它的副本,而不是訪問原始變數,如果區域性變數僅僅賦值一次那就沒什麼區別了,因此就有了這個限制
    • 總結來就是被lambda引用的區域性變數只能是final
  • 方法引用

    • 只是語法糖而已
    • 分三種方法引用

      • 靜態方法的方法引用:Integer.parseInt

        • 凡是可以用類名點出來的,那就屬於這種
        Function<String,Integer> function = Integer::parseInt;
        Consumer<String> consumer = Arrays::asList;
        • 為什麼可以這麼用,拿Integer舉例
        • 首先檢視一下Function的抽象方法的定義

          R apply(T t);
        • 如上可以看出,需要傳入一個傳出一個 T -> R
        • 再看parseInt的定義

        1. static int parseInt(String s) throws NumberFormatException {
          return parseInt(s,10);
          }

        • 如上是一個靜態方法,那麼方法引用就可以引用它,它是傳入一個String,返回一個int,那麼把上面的Function的泛型固定為String和Int不就好了,所以Function所需要的抽象方法的實現可以由paraseInt方法的實現滿足,並且泛型一致,當呼叫Function中的抽象方法的時候,傳入的引數就是傳入到parseInt方法中,並返回int,所以可以通過編譯
      • 例項的方法引用:"123".length

        List<Integer> list = new ArrayList<>();
        list.sort(Integer::compareTo);
        • 我們看為什麼可以如上的用法list.sort(Integer::compareTo);,首先檢視sort方法
        default void sort(Comparator<? super E> c) {....}
        • 是需要一個Comparator的函式式介面的抽象方法的實現
        • 再來看Comparator的抽象方法int compare(T o1, T o2);,看到這也就知道了,sort裡面需要一個(T,T)->int,那麼我們繼續看Integer的compareTo方法
        public int compareTo(Integer anotherInteger) {
            return compare(this.value, anotherInteger.value);
        }
        • 上面的compareTo也清楚的表明了是兩個引數作為傳入並且返回一個int,並且是this.value呼叫,說明是一個例項物件,所以此方法正好滿足Comparator函式式介面中抽象方法的方法簽名的定義,所以編譯無誤
      • 現有物件的例項方法引用:引用區域性變數型別中的方法

        public class Java8 {
            @Test
            public void test() throws Exception {
                Java8 java8 = new Java8();
                Function<String,Integer> function1 = str -> java8.parseInt(str);
                Function<String,Integer> function2 = java8::parseInt;
            }
            public Integer parseInt(String str){
                return Integer.parseInt(str);
            }
        }
        • 如上就是引用一個區域性變數中的方法
      • 分清第二個方法引用方法和第三個方法引用方法

        public class Java8 {
            @Test
            public void test() throws Exception {
                Java8 java8 = new Java8();
                Function<String,Integer> function2 = str -> str.length();
                Function<String,Integer> function22 = String::length;
                Function<String,Integer> function3 = str -> java8.length(str);
                Function<String,Integer> function33 = java8::length;
            }
            public Integer length(String str){
                return str.length();
            }
        }
        • 如上方法名我做了區別,可以觀察出,例項方法引用就是靠傳入的引數呼叫方法,而現有物件例項方法利用是不用引用傳入的引數作為呼叫的物件的
  • 構造方法的引用

    • 這個對於方法引用就簡單太多了,舉例說明一下
    • 對於無參的構造器
    Supplier<Apple> supplier = Apple::new;
    • 對於一個引數的
    Function<String,Apple> function = Apple::new;
    • 對於兩個引數的
    BiFunction<String,Integer,Apple> function = Apple::new;
    • 總結來就是找能與想建立的物件的建構函式引數個數匹配的函式式介面,並且函式式介面的泛型必須跟構造方法引數型別一致
    • 上面雖然沒有例項化物件,但是已經引用了此物件,那麼就會發生一些有意思的事情
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Fruit {
        private Integer weight;
    }
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Apple extends Fruit {
        private Integer weight;
    }
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Banana extends Fruit{
        private Integer weight;
    }
    public class Java8 {
        @Test
        public void test() throws Exception {
            Map<String,Function<Integer,Fruit>> map = new HashMap<>();
            map.put("apple",Apple::new);
            map.put("banana",Banana::new);
    
            Fruit apple = get(map, "apple", 123);
            System.out.println(apple);
            Fruit banana = get(map, "banana", 23);
            System.out.println(banana);
        }
        public Fruit get(Map<String,Function<Integer,Fruit>> map,String fruitName,Integer fruitWeight){
            return map.get(fruitName).apply(fruitWeight);
        }
    }
    //輸出:Apple(weight=123)
    //輸出:Banana(weight=23)
    • 可以根據不同的名稱得到不同的重量的各種蔬果,只因為它存在構造器的引用
  • 下面就是lambda表示式的有用的方法的說明

    • 考慮一種情況那就是組合比較,比如說兩個物件,比較值a都相同的情況下在比較b值是否相等,然後排序,如上的lambda中我們只是使用Comparator中的方法比較了一次,那麼如果我們是連續比較呢?

      @Test
      public void test() throws Exception {
          Apple apple1 = new Apple(2015,22);
          Apple apple2 = new Apple(2014,82);
          Apple apple3 = new Apple(2014,12);
          List<Apple> apples = Arrays.asList(apple1,apple2,apple3);
          apples.sort(Comparator.comparing(Apple::getYear));
          //[Apple(year=2014, weight=82), Apple(year=2014, weight=12), Apple(year=2015, weight=22)]
          System.out.println(apples);
      }
      • 如上使用lambda只能排序一次,那麼我們怎麼二次排序呢?使用thenComparing就可以啦
      @Test
      public void test() throws Exception {
          Apple apple1 = new Apple(2015,22);
          Apple apple2 = new Apple(2014,82);
          Apple apple3 = new Apple(2014,12);
          List<Apple> apples = Arrays.asList(apple1,apple2,apple3);
          apples.sort(Comparator.comparing(Apple::getYear).thenComparing(Apple::getWeight));
          //[Apple(year=2014, weight=12), Apple(year=2014, weight=82), Apple(year=2015, weight=22)]
          System.out.println(apples);
      }
      • 翻轉的排序規則的話使用reversed就可以實現了
    • 在我們使用Predicate判斷介面的時候,怎麼實現&&,||,^呢?對應的方法也就是and,or,negate

      • 還是利用上面的apples集合,下面我們來做篩選
      • 篩選出2015年的apple
      Predicate<Apple> year2014 = apple -> apple.getYear() == 2014;
      for (Apple apple : apples) {
          if (year2014.test(apple)) {
              //Apple(year=2014, weight=82)
              //Apple(year=2014, weight=12)
              System.out.println(apple);
          }
      }
      Predicate<Apple> year2015 = year2014.negate();
      for (Apple apple : apples) {
          if (year2015.test(apple)) {
              //Apple(year=2015, weight=22)
              System.out.println(apple);
          }
      }
      • negate也就是取反操作,當然取2015年的apple,直接==2015也行,這只是在做演示
    • 取出蘋果weight=12或者22的apple
    Predicate<Apple> weight12 = apple -> apple.getWeight() == 12;
    Predicate<Apple> weight22 = weight12.or(apple -> apple.getWeight() == 22);
    for (Apple apple : apples) {
        //Apple(year=2015, weight=22)
        //Apple(year=2014, weight=12)
        if (weight22.test(apple)){
            System.out.println(apple);
        }
    }
    • 只是Predicate的組合而已
    • 取出weight=12,year=2014的aple
Predicate<Apple> weight12 = apple -> apple.getWeight() == 12;
Predicate<Apple> year2014 = weight12.and(apple -> apple.getYear() == 2014);
for (Apple apple : apples) {
    //Apple(year=2014, weight=12)
    if (year2014.test(apple)){
        System.out.println(apple);
    }
}
//Predicate<Apple> and = weight12.and(year2014);這樣也是可以的,而且這樣標示比較的清楚,只要變數名起的好
  • 書上說:這樣可以組合更復雜的表示式,但是一行行去讀程式碼還不如原來的操作符。因為自己也是學習,所以理解不深刻,對於這如果有其他的爽歪歪的用法歡迎評論~
  • 函式的複合

    • 在Function中存在andThen方法,我們來試試看
      public void test() throws Exception {
      Function<Integer,Integer> a = x -> x + 1;
      Function<Integer,Integer> b = x -> x * 2;
      Function<Integer,Integer> c = a.andThen(b);
      System.out.println(c.apply(3));
      //1 4
      //2 6
      //3 8
    • 明顯的看出,傳入的3是這樣計算的(3+1)*2
    • 還提供了另一個方法:compose
Function<Integer,Integer> a = x -> x + 1;
Function<Integer,Integer> b = x -> x * 2;
Function<Integer,Integer> c = a.compose(b);
System.out.println(c.apply(2));
//1 3
//2 5
//3 7
  • 計算過程就是這樣的:(2*2)+1

好了本篇就介紹到這了。如果發現不對的,請及時更正哈~


相關文章