文章出處

一、什么是lambda表達式?

     Lambda 是一個匿名函數,我們可以把 Lambda 表達式理解為是一段可以傳遞的代碼(將代碼像數據一樣進行傳遞)。可以寫出更簡潔、更靈活的代碼。作為一種更緊湊的代碼風格,使 Java的語言表達能力得到了提升。

匿名內部類的寫法:

public void demo1(){
    
    Comparator<Integer> comparator = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return Integer.compare(o1, o2);
        }
    };
    
    Runnable runnable = new Runnable() {
        @Override
        public void run() {}
    };
}

這樣寫會發現一個問題,實現的方法是冗余的代碼,實際當中并沒有什么用處。我們看看Lambda的寫法。

Lambda表達式的寫法

public void demo2(){
    
    Comparator<Integer> comparator = (x,y) -> Integer.compare(x, y);
    
    Runnable runnable = () -> System.out.println("lambda表達式");
}

我們會發現Lambda表達式的寫法更加的簡潔、靈活。它只關心參數和執行的功能(具體需要干什么,比如->后的Integer.compare(x, y))。

二、lambda表達式語法

lambda表達式的一般語法:

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

包含三個部分:參數列表,箭頭(->),以及一個表達式或語句塊。

1.一個括號內用逗號分隔的形式參數,參數是函數式接口里面方法的參數

2.一個箭頭符號:->

3.方法體,可以是表達式和代碼塊,方法體是函數式接口里面方法的實現,如果是代碼塊,則必須用{}來包裹起來,且需要一個return 返回值,但有個例外,若函數式接口里面方法返回值是void,則無需{}。

總體看起來像這樣:

(parameters) -> expression 或者 (parameters) -> { statements; }

上面的lambda表達式語法可以認為是最全的版本,寫起來還是稍稍有些繁瑣。別著急,下面陸續介紹一下lambda表達式的各種簡化版:

1. 參數類型省略–絕大多數情況,編譯器都可以從上下文環境中推斷出lambda表達式的參數類型。這樣lambda表達式就變成了:

(param1,param2, ..., paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

2. 單參數語法:當lambda表達式的參數個數只有一個,可以省略小括號。lambda表達式簡寫為:

param1 -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

3. 單語句寫法:當lambda表達式只包含一條語句時,可以省略大括號、return和語句結尾的分號。lambda表達式簡化為:

param1 -> statment

下面看幾個例子:

demo1:無參,無返回值,Lambda 體只需一條語句

Runnable runnable = () -> System.out.println("lamda表達式");

demo2:Lambda 只需要一個參數

Consumer<String> consumer=(x)->System.out.println(x);

demo3:Lambda 只需要一個參數時,參數的小括號可以省略

Consumer<String> consumer=x->System.out.println(x);

demo4:Lambda 需要兩個參數

Comparator<Integer> comparator = (x, y) -> Integer.compare(x, y);

demo5:當 Lambda 體只有一條語句時,return 與大括號可以省略

BinaryOperator<Integer> binaryOperator=(x,y)->(x+y);

demo6:數據類型可以省略,因為可由編譯器推斷得出,稱為“類型推斷”

BinaryOperator<Integer> bo=(x,y)->{
System.out.println("Lambda");
return x+y;};

類型推斷

    上述 Lambda 表達式中的參數類型都是由編譯器推斷得出的。Lambda 表達式中無需指定類型,程序依然可以編譯,這是因為 javac 根據程序的上下文,在后臺推斷出了參數的類型。Lambda 表達式的類型依賴于上下文環境,是由編譯器推斷出來的。這就是所謂的 “類型推斷”。

三、lambda表達式的類型

我們都知道,Java是一種強類型語言。所有的方法參數都有類型,那么lambda表達式是一種什么類型呢?

View.OnClickListener listener = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //...
    }
};
        
button.setOnClickListener(listener);

如上所示,以往我們是通過使用單一方法的接口來代表一個方法并且重用它。

在lambda表達式中,仍使用的和之前一樣的形式。我們叫做函數式接口(functional interface)。如我們之前button的點擊響應事件使用的View.OnClickListener就是一個函數式接口。

public class View implements Drawable.Callback, KeyEvent.Callback,
    AccessibilityEventSource {
    ...
    
    public interface OnClickListener {
        void onClick(View v);
    }
    ...
}

那究竟什么樣的接口是函數式接口呢?

     函數式接口是只有一個抽象方法的接口,用作表示lambda表達式的類型。 比如Java標準庫中的java.lang.Runnable和java.util.Comparator都是典型的函數式接口。java 8提供 @FunctionalInterface作為注解,這個注解是非必須的,只要接口符合函數式接口的標準(即只包含一個方法的接口),虛擬機會自動判斷,但最好在接口上使用注解@FunctionalInterface進行聲明,以免團隊的其他人員錯誤地往接口中添加新的方法。舉例如下:

@FunctionalInterface
public interface Runnable { void run(); }

public interface Callable<V> { V call() throws Exception; }

public interface ActionListener { void actionPerformed(ActionEvent e); }

public interface Comparator<T> { 
    int compare(T o1, T o2); 
    
    boolean equals(Object obj); 
}

     注意最后這個Comparator接口。它里面聲明了兩個方法,貌似不符合函數接口的定義,但它的確是函數接口。這是因為equals方法是Object的,所有的接口都會聲明Object的public方法——雖然大多是隱式的。所以,Comparator顯式的聲明了equals不影響它依然是個函數接口。

     Java中的lambda無法單獨出現,它需要一個函數式接口來盛放,lambda表達式方法體其實就是函數接口的實現。即Lambda表達式不能脫離目標類型存在,這個目標類型就是函數式接口,看下面的例子: 

String []datas = new String[] {"peng","zhao","li"};
Comparator<String> comp = (v1,v2) -> Integer.compare(v1.length(), v2.length());
Arrays.sort(datas,comp);
Stream.of(datas).forEach(param -> {System.out.println(param);}); 

Lambda表達式被賦值給了comp函數接口變量。

你可以用一個lambda達式為一個函數接口賦值:

Runnable r1 = () -> {System.out.println("Hello Lambda!");};

 然后再賦值給一個Object:

Object obj = r1;

但卻不能這樣干:

 Object obj = () -> {System.out.println("Hello Lambda!");}; // ERROR! Object is not a functional interface!

必須顯式的轉型成一個函數接口才可以:

Object o = (Runnable) () -> { System.out.println("hi"); }; // correct

一個lambda表達式只有在轉型成一個函數接口后才能被當做Object使用。所以下面這句也不能編譯:

 System.out.println( () -> {} ); //錯誤! 目標類型不明

必須先轉型:

System.out.println( (Runnable)() -> {} ); // 正確

假設你自己寫了一個函數接口,長的跟Runnable一模一樣:

@FunctionalInterface
public interface MyRunnable {
    public void run();
}

那么

Runnable r1 =    () -> {System.out.println("Hello Lambda!");};
MyRunnable2 r2 = () -> {System.out.println("Hello Lambda!");};

都是正確的寫法。這說明一個lambda表達式可以有多個目標類型(函數接口),只要函數匹配成功即可。但需注意一個lambda表達式必須至少有一個目標類型。

JDK預定義了很多函數接口以避免用戶重復定義。最典型的是Function:

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

這個接口代表一個函數,接受一個T類型的參數,并返回一個R類型的返回值。另一個預定義函數接口叫做Consumer,跟Function的唯一不同是它沒有返回值。

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

還有一個Predicate,用來判斷某項條件是否滿足。經常用來進行篩濾操作:

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

綜上所述,一個lambda表達式其實就是定義了一個匿名方法,只不過這個方法必須符合至少一個函數接口。

四、lambda表達式可使用的變量

先舉例:

@Test
public void test1(){
    //將為列表中的字符串添加前綴字符串
    String waibu = "lambda :";
    List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
    List<String>execStrs = proStrs.stream().map(chuandi -> {
        Long zidingyi = System.currentTimeMillis();
        return waibu + chuandi + " -----:" + zidingyi;
    }).collect(Collectors.toList());
    
    execStrs.forEach(System.out::println);
}

輸出:

lambda :Ni -----:1498722594781
lambda :Hao -----:1498722594781
lambda :Lambda -----:1498722594781

變量waibu :外部變量

變量chuandi :傳遞變量

變量zidingyi :內部自定義變量

     lambda表達式可以訪問給它傳遞的變量,訪問自己內部定義的變量,同時也能訪問它外部的變量。不過lambda表達式訪問外部變量有一個非常重要的限制:變量不可變(只是引用不可變,而不是真正的不可變)。

當在表達式內部修改waibu = waibu + " ";時,IDE就會提示你:

Local variable waibu defined in an enclosing scope must be final or effectively final

編譯時會報錯。因為變量waibu被lambda表達式引用,所以編譯器會隱式的把其當成final來處理

以前Java的匿名內部類在訪問外部變量的時候,外部變量必須用final修飾。現在java8對這個限制做了優化,可以不用顯示使用final修飾,但是編譯器隱式當成final來處理。

五、lambda表達式作用域

總體來說,Lambda表達式的變量作用域與內部類非常相似,只是條件相對來說,放寬了些,以前內部類要想引用外部類的變量,必須像下面這樣 

final String[] datas = new String[] { "peng", "Zhao", "li" };
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(datas);
    }
}).start();

將變量聲明為final類型的,現在在Java 8中可以這樣寫代碼

String []datas = new String[] {"peng","Zhao","li"};
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(datas);
    }
}).start();

也可以這樣寫:

new Thread(() -> System.out.println(datas)).start();

     看了上面的兩段代碼,能夠發現一個顯著的不同,就是Java 8中內部類或者Lambda表達式對外部類變量的引用條件放松了,不要求強制的加上final關鍵字了,但是Java 8中要求這個變量是effectively final。What is effectively final?

Effectively final就是有效只讀變量,意思是這個變量可以不加final關鍵字,但是這個變量必須是只讀變量,即一旦定義后,在后面就不能再隨意修改,如下代碼會編譯出錯

String []datas = new String[] {"peng","Zhao","li"};
datas = null;
new Thread(() -> System.out.println(datas)).start();

Java中內部類以及Lambda表達式中也不允許修改外部類中的變量,這是為了避免多線程情況下的race condition。

六、lambda表達式中的this概念

在lambda中,this不是指向lambda表達式產生的那個對象,而是聲明它的外部對象。

例如:

package com.demo;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class WhatThis {
    
    public void whatThis(){
        //轉全小寫
         List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
        List<String> execStrs = proStrs.stream().map(str -> {
             System.out.println(this.getClass().getName());
             return str.toLowerCase();
        }).collect(Collectors.toList());
        
        execStrs.forEach(System.out::println);
    }
    
    public static void main(String[] args) {
        WhatThis wt = new WhatThis();
        wt.whatThis();
    }

}

輸出:

com.wzg.test.WhatThis
com.wzg.test.WhatThis
com.wzg.test.WhatThis
ni
hao
lambda

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()