程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java 8 新特性:Lambda 表達式 ——諾諾"塗鴉"記憶

Java 8 新特性:Lambda 表達式 ——諾諾"塗鴉"記憶

編輯:JAVA綜合教程

Java 8 新特性:Lambda 表達式 ——諾諾"塗鴉"記憶


Lambda 表達式

 

(注:此文乃個人查找資料然後學習總結的,若有不對的地方,請大家指出,非常感謝!另外,知識都有串聯,如果某一處看不懂,就先接著往下看,之後再回頭看不明白的地方就會恍然大悟了。)

 

一.為什麼Java需要Lambda表達式?

 

如果忽視注解(Annotations)、泛型(Generics)等特性,自Java語言誕生時起,它的變化並不大。Java一直都致力維護其對象至上的特征,在使用過JavaScript之類的函數式語言之後,Java如何強調其面向對象的本質,以及源碼層的數據類型如何嚴格變得更加清晰可感。其實,函數對Java而言並不重要,在Java的世界裡,函數無法獨立存在。

 

然而,在函數式編程語言中,函數是一等公民,它們可以獨立存在,你可以將其賦值給一個變量,或將他們當做參數傳給其他函數。JavaScript是最典型的函數式編程語言。函數式語言提供了一種強大的功能——閉包,相比於傳統的編程方法有很多優勢,閉包是一個可調用的對象,它記錄了一些信息,這些信息來自於創建它的作用域。Java現在提供的最接近閉包的概念便是Lambda表達式,雖然閉包與Lambda表達式之間存在顯著差別,但至少Lambda表達式是閉包很好的替代者。

 

Lambda表達式為Java添加了缺失的函數式編程特點,使我們能將函數當做一等公民看待。盡管不完全正確,我們很快就會見識到Lambda與閉包的不同之處,但是又無限地接近閉包。在支持一類函數的語言中,Lambda表達式的類型將是函數。但是,在Java中,Lambda表達式是對象,他們必須依附於一類特別的對象類型——函數式接口(functional interface)。

 

二.函數式接口

 

函數式接口是只包含一個抽象方法的接口。函數式接口有時候被稱為SAM類型,意思是單抽象方法(Single Abstract Method)。

 

一般來說,這個抽象方法指明了接口的目標用途。因此,函數式接口通常表示單個動作。

 

eg:標准接口Runnable是一個函數式接口,因為它只定義了一個方法run();因此,run()定義了Runnable的動作。

 

此外,函數式接口定義了lambda表達式的目標類型。但lambda表達式只能用於其目標類型已經被指定的上下文中。

 

同時,Java 8引入了一個新的注解:@FunctionalInterface。

 

可以在任意函數式接口上面使用 @FunctionalInterface 來標識它是一個函數式接口,但是該注解不是強制的。

 

 

1)當你注釋的接口不是有效的函數式接口時,可以使用 @FunctionalInterface 解決編譯層面的錯誤。

 

eg:

 

自定義一個函數式接口:

 

@FunctionalInterface
public interface MyTestInterface {
    public void doSomeThing();
}

 

據定義,函數式接口只能有一個抽象方法,如果你嘗試添加第二個抽象方法,將拋出編譯時錯誤。

錯誤寫法:

@FunctionalInterface
public interface MyTestInterface {
    public void doSomeThing();
    public void doMoreThing();
}

提示錯誤信息:

\

 

2)當lambda表達式被轉換成一個函數式接口的實例時,需要注意處理檢查時異常。

eg:

 

Runnable runnable = () -> {
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
            }
 };

 

若不加 try catch 語句,賦值語句就會編譯錯誤,因為Runnable的run方法是沒有異常拋出的。

3)Callable 是可以拋出任何異常,並且有返回值,當需要不返回任何數據時可這樣定義:

Callable callable = () -> {
            System.out.println("zzzzz");
            return null;
};

 

 

Note:

 

 

1)函數式接口可以定義Object定義的任何公有方法,例如equals(),而不影響其作為"函數式接口"的狀態。Object的公有方法被視為函數式接口的隱式成員,因為函數式接口的實例會默認自動實現它們。

 

 

2)默認方法和靜態方法(後面會具體解釋)不會違反函數接口的約定。

 

 

三.Lambda表達式

 

1.簡介

 

在Java中,Lambda 表達式(lambda expression)是一個匿名函數。

 

Lambda表達式基於數學中的λ演算得名,直接對應於其中的Lambda抽象(lambda abstraction),是一個匿名函數,即沒有函數名的函數。Lambda表達式可以表示閉包,但又不同於函數式語言的閉包。Lambda表達式讓代碼變得簡潔並且允許你傳遞行為,在java8出現之前傳遞行為的方法只有通過匿名內部類。

 

Lambda表達式的Java實現:第一個就是Lambda表達式自身,第二個是函數式接口。

 

Lambda表達式本質上就是一個匿名(即未命名的方法)。但是這個方法是不能獨立執行的,而是用於實現由函數式接口定義的一個方法(即:使用 Lambda 表達式實例化函數式接口)。因此,Lambda表達式會導致產生一個匿名類。

 

 

Lambda表達式不是獨立執行的,而是構成了一個函數式接口定義的抽象方法的實現,該函數式接口定義了它的目標類型。結果,只有在定義了lambda表達式的目標類型的上下文中,才能使用該表達式。當把一個lambda表達式賦給一個函數式接口的引用時,就創建了這樣的上下文。

當目標類型上下文中出現lambda表達式時,就會自動創建實現了函數式接口的一個類的實例(類似於匿名類),函數式接口聲明的抽象方法的行為由lambda表達式定義。當通過目標調用該方法時,就會執行lambda表達式。

為了在目標類型上下文中使用lambda表達式,抽象方法的類型和lambda表達式的類型必須兼容。eg:如果抽象方法指定了兩個int類型的參數,那麼lambda表達式也必須執行兩個參數,其類型要麼被顯示指定為int類型,要麼在上下文中可以被隱式的推斷為int類型。總的來講lambda表達式的參數的類型和數量必須與函數式接口內的抽象方法的參數兼容;返回類型必須兼容;並且lambda表達式可能拋出的異常必須能被該方法接受。

2.優點

Lambda表達式的應用使代碼變得更加緊湊,可讀性增強;Lambda表達式使並行操作大集合變得更方便,可以充分發揮多核CPU的優勢,更易於為多核處理器編寫代碼。

 

 

3.組成(或定義)

Lambda 表達式由三個部分組成:

 

 

第一部分為一個括號內用逗號分隔的形式參數,參數是函數式接口裡面方法的參數;

 

 

第二部分為一個箭頭符號:->

 

 

第三部分為方法體,可以是表達式和代碼塊。

 

 

語法:

 

 

1)方法體為表達式,該表達式的值作為返回值返回。

 

 

(parameters)->expression

 

 

2)方法體為代碼塊,必須用{}來包裹起來,且需要一個return返回值,但若函數式接口裡面方法返回值是void,則無需返回值。

 

 

(parameters) -> { statements; }

 

 

Note:

1) 一個Lambda表達式可以有零個或多個參數
2) 參數的類型既可以明確聲明,也可以根據上下文來推斷。
eg:(int a)與(a)效果相同
3) 所有參數需包含在圓括號內,參數之間用逗號相隔。
eg:(a, b)或(int a, int b)或(String a, int b, float c)
4) 空圓括號代表參數集為空。
eg:() -> 50
5) 當只有一個參數,且其類型可推導時,圓括號()可省略。
eg:a -> return a*a
6) Lambda表達式的主體可包含零條或多條語句
7) 如果Lambda表達式的主體只有一條語句,花括號{}可省略。匿名函數的返回類型與該主體表達式一致。

 

8) 如果 Lambda 表達式的主體包含一條以上語句,則表達式必須包含在花括號{}中(形成代碼塊)。匿名函數的返回類型與代碼塊的返回類型一致,若沒有返回則為空。

9)對於Lambda表達式中的多個參數,如果需要顯示聲明一個參數的類型,那麼必須為所有的參數都聲明類型。

eg:MyNumericTest是一個自定義函數式接口,詳情接口代碼略,看下面應用的例子:

這樣寫不合法:MyNumericTest isFactor = (int n,d) -> (n%d)==0;

正確的寫法:MyNumericTest isFactor = (int n,int d) -> (n%d)==0;

 
 
 

 

Lambda寫法示例:

eg:

1)左邊是指定類型的逗號分割的輸入列表,右邊是帶有return的代碼塊:

(intx,inty) -> {returnx + y; }

2)左邊是推導類型的逗號分割的輸入列表,右邊是返回值:

(x, y) -> x + y

3)左邊是推導類型的單一參數,右邊是一個返回值:

x -> x * x

4)左邊沒有輸入 (官方名稱: "burger arrow"),在右邊返回一個值:

() -> x

5)左邊是推導類型的單一參數,右邊是沒返回值的代碼塊(返回void):

x -> { System.out.println(x); }

6)靜態方法引用:(注:第一次看不懂沒關系,後面會提到這種用法)

String::valueOf

7)非靜態方法引用:

Object::toString

8)繼承的函數引用:

x::toString

9)構造函數引用:

ArrayList::new

 

4.類型推斷

 

在Lambda表達式中,我們不需要明確指出參數類型,javac編譯器會通過上下文自動推斷參數的類型信息。根據上下文推斷類型的行為稱為類型推斷

 

Java8提升了Java中已經存在的類型推斷系統,使得對Lambda表達式的支持變得更加強大。javac會尋找緊鄰lambda表達式的一些信息通過這些信息來推斷出參數的正確類型。

 

Note:在大多數情況下,javac會根據上下文自動推斷類型。假設因為丟失了上下文信息或者上下文信息不完整而導致無法推斷出類型,代碼編譯就不會通過。

 

JDK英文文檔:Generalized Target-Type Inference


 

 

5.Lambda 表達式的使用示例

(注:下面的例子若暫時看不懂,可通篇博客看完後再回過來看示例,第一次看難免會覺得不理解。)

1)無參Lambda表達式:

 

//函數式接口 
    interface MyNumber
    {
        double getValue();
    }

    class lambdaDemo
    {
        public static void main(String[] args)
        {
            MyNumber myNum;

            myNum = ()->123.45;
            System.out.println("A fixed value: "+myNum.getValue());

            myNum = ()->Math.random()*100;
            System.out.println("A random value: "+myNum.getValue());

            //下面情況是:lambda表達式的返回值類型與函數式接口中抽象函數的類型不匹配。
			// myNum = ()->"123.03"    //error! 
        }
    }

 

2)帶參數的lambda表達式:

 

interface NumericTest
    {
        boolean test(int n);
    }

    class lambdaDemo2
    {
        public static void main(String[] args)
        {
            NumericTest isEven = (n)->(n%2)==0;
            if(isEven.test(10)){
				System.out.println("10 is even");
			}
            if(!isEven.test(9)){
				System.out.println("9 is not even");
			}

            NumericTest isNonNeg = (n)-> n>=0;
            if(isNonNeg.test(1)){
				System.out.println("1 is non-negative");
			}
            if(!isNonNeg.test(-1)){
				System.out.println("-1 is negative");
			}
        }
    }

 

 

3)接受兩個參數的lambda表達式:

 

interface NumericTest2
    {
        boolean test(int n,int d);
    }

    class lambdaDemo3
    {
        public static void main(String[] args)
        {
            //測試一個數字是否是另一個數字的因子。
            NumericTest2 isFactor = (n,d) -> (n%d)==0;

            if(isFactor.test(10,2)){ 
				System.out.println("2 is a factor of 10");
			}
            if(!isFactor.test(10,3)){ 
				System.out.println("3 is not a factor of 10");
			}
        }
    }

 

 

4)線程優化:

原線程:

 

 new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from thread");
            }
        }).start();

 

使用Lambda表達式:

 

new Thread(
                () -> System.out.println("Hello from thread")
        ).start();

 

5)事件處理

向一個 UI 組件添加 ActionListener

原java寫法:

 

button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("The button was clicked using old java code!");
            }
        });

 

使用Lambda表達式:

 

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

 

6)打印出給定數組中的所有元素

Note:使用 Lambda 表達式的方法不止一種。

原java寫法:

 

List list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer n : list) {
            System.out.println(n);
        }
使用Lambda表達式:

 

第一種方式:

 

List list = Arrays.asList(1, 2, 3, 4, 5);
        list.forEach(n -> System.out.println(n));

 

第二種方式:(這裡用到了方法引用)

 

List list = Arrays.asList(1, 2, 3, 4, 5);
        list.forEach(System.out::println);

 

7)使用斷言(Predicate)函數式接口創建一個測試,並打印所有通過測試的元素

代碼:

 

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class LambdaTest {
    public static void main(String[] a) {
        List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

        System.out.println("all numbers:");
        evaluate(list, (n) -> true);

        System.out.println("no numbers:");
        evaluate(list, (n) -> false);

        System.out.println("even numbers:");
        evaluate(list, (n) -> n % 2 == 0);

        System.out.println("odd numbers:");
        evaluate(list, (n) -> n % 2 == 1);

        System.out.println("numbers greater than 5:");
        evaluate(list, (n) -> n > 5);
    }
    public static void evaluate(List list, Predicate predicate) {
        for(Integer n: list)  {
            if(predicate.test(n)) {
                System.out.println(n + " ");
            }
        }
    }
}
結果顯示:

 

 

all numbers: 1 2 3 4 5 6 7 
no numbers: 
even numbers: 2 4 6 
odd numbers: 1 3 5 7 
numbers greater than 5: 6 7

 

8)Lambda 表達式打印數值中每個元素的平方

以下使用了 .stream() 方法將常規數組轉化為流。Java 8 新增加了流 APIs。java.util.stream.Stream接口包含許多有用的方法,能結合 Lambda 表達式產生非常棒的效果。我們將 Lambda 表達式x -> x*x傳給 map() 方法,該方法會作用於流中的所有元素。之後,我們使用 forEach 方法打印數據中的所有元素。

原java實現方式:

 

 List list = Arrays.asList(0,1,2,3,4,5);
        for(Integer n : list) {
            int x = n * n;
            System.out.println(x);
        }

 

使用Lambda表達式和Java8新特性 流 實現方式:

 

List list = Arrays.asList(0,1,2,3,4,5);
        list.stream().map((x) -> x*x).forEach(System.out::println);

 

9)計算給定數值中每個元素平方後的總和

原java實現方式:

 

 List list = Arrays.asList(1,2,3,4,5);
        int sum = 0;
        for (Integer n : list) {
            int x = n * n;
            sum = sum + x;
        }
        System.out.println(sum);

 

使用Lambda表達式:

 

List list = Arrays.asList(1,2,3,4,5);
        int sum = list.stream().map(x -> x * x).reduce((x, y) -> x + y).get();
        System.out.println(sum);

 

6.塊Lambda表達式

顯示的Lambda方法體內只包含單個表達式,這種類型的Lambda體被稱為表達式體,具有表達式體的Lambda表達式可以被稱為表達式Lambda。在表達式體中,操作符右側的代碼必須包含單獨一個表達式。

Java支持另外一種類型的Lambda表達式,其中操作符右側的代碼可以由一個代碼塊構成,其中可以包含多條語句。這種類型的Lambda體被稱為塊體。具有塊體的Lambda表達式有時候被稱為塊Lambda

塊Lambda表達式擴展了Lambda表達式內部可以處理的操作類型,因為它允許Lambda體包含多條語句。創建塊Lambda很容易,只需要使用花括號包圍Lambda體。

在塊Lambda中必須顯示使用return語句來返回值。必須這麼做,因為塊Lambda體代表的不是一個單獨的表達式。

Note:當lambda表達式中出現return語句時,只是從lambda體返回,而不會導致包圍lambda體的方法返回。

eg:使用塊lambda來計算並返回一個int類型值的階乘

 

interface NumericFunc {
    inf func(int n);
}
class BlockLambdaDemo {
    public static void main(String[] args) {
        NumericFunc factorial = (n) ->
        {
            int result = 1;
            for (int i = 1; i <= n; i++) {
                result = i * result;
            }
            return result;
        };
        System.out.println("The factorial of 3 is " + factorial.func(3));
        System.out.println("The factorial of 5 is " + factorial.func(5));
    }
}

 

 

7.泛型函數式接口

Lambda表達式自身不能指定類型參數。因此,Lambda表達式不能是泛型(當然,由於存在類型推斷,所有Lambda表達式都展現出了一些類似於泛型的特征)。但是,與Lambda表達式關聯的函數式接口可以泛型。此時,Lambda表達式的目標類型部分由聲明函數式接口引用時指定的參數類型決定。

 

eg:

 

interface SomeFunc {
    T func(T t);
}
class GenericFunctionalInterfaceDemo {
    public static void main(String[] args) {
        SomeFunc reverse = (str) ->
        {
            int i;
            String result = "";
            for (i = str.length() - 1; i >= 0; i--) {
                result += str.charAt(i);
            }
            return result;
        };
        System.out.println("lambda reserved is " + reverse.func("lambda"));
        
        SomeFunc factorial = (n) ->
        {
            int result = 1;

            for (int i = 1; i <= n; i++) {
                result = result * i;
            }
            return result;
        };
        System.out.println("The factorial of 3 is " + factorial.func(3));
    }
}

結果:

 

lambda reserved is adbmal
The factorial of 3 is 6

 

分析:

T指定了func()函數的返回類型和參數類型。這意味著它與任何 只接收一個參數,並返回一個相同類型的值的lambda表達式兼容。

SomeFunc接口用於提供對兩種不同類型的lambda表達式的引用。第一種表達式使用String類型,第二種表達式使用Integer類型。因此,同一個函數式接口可以用於reserve lambda表達式和factorial lambda表達式。區別僅在於傳遞給SomeFunc的參數類型。

 

8.作為參數傳遞的Lambda表達式

為了將lambda表達式作為參數傳遞,接收lambda表達式的參數的類型必須是與該lambda表達式兼容的函數式接口的類型。

eg:Lambda表達式 作為方法參數使用

 

//Use lambda expressions as an argument to method
interface StringFunc {
    String func(String n);
}
class lambdasAsArgumentsDemo {
    static String stringOp(StringFunc sf, String s) {
        return sf.func(s);
    }
    public static void main(String[] args) {
        String inStr = "lambda add power to java";
        String outStr;
        System.out.println("Here is input string: " + inStr);
        //Lambda表達式 作為方法參數使用
        //第一種方式
        outStr = stringOp((str) -> str.toUpperCase(), inStr);
        System.out.println("The string in uppercase: " + outStr);
        //第二種方式
        outStr = stringOp((str) ->
        {
            String result = "";
            for (int i = 0; i < str.length(); i++) {
                if (str.charAt(i) != '') {
                    result += str.charAt(i);
                }
            }
            return result;
        }, inStr);
        System.out.println("The string with spaces removed: " + outStr);
        //第三種方式
        //當塊lambda看上去特別長,不適合嵌入到方法的調用中時,很容易把塊lambda賦給一個函數式接口變量.
        //然後,可以簡單地把該引用傳遞給方法。
        StringFunc reverse = (str) ->
        {
            String result = "";
            for (int i = str.length() - 1; i >= 0; i--) {
                result += str.charAt(i);
            }
            result result;
        };
        System.out.println("The string reserved: " + stringOp(reverse, inStr));
    }
}

 

輸出結果:

Here is input string: lambda add power to java
The string in uppercase: LAMBDAS ADD POWER TO JAVA
The string with spaces removed: lambdaaddpowertojava
The string reserved: avaJ ot rewop dda sadbmal

分析:

首先注意stringOp()方法。它有兩個參數,第一個參數的類型是StringFunc,而StringFunc是一個函數式接口。因此,這個參數可以接受對任何StringFunc實例的引用,包括由lambda表達式創建的實例。stringOp的第二個參數是String類型,也就是要操作的字符串。接下來,注意對stringOp()的第一次調用,如下所示:
outStr = stringOp((str)->str.uppercase(),inStr);

這裡,傳遞了一個簡單的表達式lambda作為參數。這會創建函數式接口StringFunc的一個實例,並把對該實例的一個引用傳遞給stringOp()方法的第一個參數這就把嵌入在一個類實例中的lambda代碼傳遞給了方法。目標類型上下文由參數的類型決定。因此lambda表達式與該類型兼容,調用是合法的。

當塊lambda看上去特別長,不適合嵌入到方法的調用中時,很容易把塊lambda賦給一個函數式接口變量,正如上面代碼中那樣。然後,可以簡單地把該引用傳遞給方法。

 

9.為什麼抽象類不能通過利用lambda實例化?

 

(注:此處可以忽略.在網上看到有這樣的說法,目前此處本人也不是很清晰,先記錄下來,以後明白了再補充。)

抽象類,哪怕只聲明了一個抽象方法,也不能使用lambda來實例化。
下面有兩個類Ordering和CacheLoader,都帶有一個抽象方法,摘自於Guava庫。如果能夠聲明它們的實例,像這樣使用lambda表達式麼?
Ordering order = (a, b) -> ...;
CacheLoader loader = (key) -> ...;
 

1)這樣做會增加閱讀lambda的難度。

以這種方式實例化一段抽象類將導致隱藏代碼的執行:抽象類的構造方法。

2)它拋出了lambda表達式可能的優化。

在未來,它可能是這種情況,lambda表達式都不會計算到對象實例。放任用戶用lambda來聲明抽象類將妨礙像這樣的優化。

 

此外,有一個簡單地解決方法。事實上,上述兩個摘自Guava庫的實例類已經證明了這種方法。增加工廠方法將lambda轉換成實例。
Ordering order = Ordering.from((a, b) -> ...);

 

CacheLoader loader = CacheLoader.from((key) -> ...);

 

 

10.lambda表達式是否只是一個匿名內部類的語法?

答案是NO。原因有兩點:

 

 

·性能影響: 假如lambda表達式是采用匿名內部類實現的,那麼每一個lambda表達式都會在磁盤上生成一個class文件。當JVM啟動時,這些class文件會被加載進來,因為所有的class文件都需要在啟動時加載並且在使用前確認,從而會導致JVM的啟動變慢。

 

 

·向後的擴展性:如果Java8的設計者從一開始就采用匿名內部類的方式,那麼這將限制lambda表達式未來的使發展范圍。

 

11.匿名類與 lambda表達式的區別:

1)在匿名類中,this 指代的是匿名類本身;而在lambda表達式中,this指代的是lambda表達式所在的這個類。

2)lambda表達式的類型是由上下文決定的,而匿名類中必須在創建實例的時候明確指定。

3)Lambda 表達式的編譯方法是:Java 編譯器編譯 Lambda 表達式並將他們轉化為類裡面的私有函數,它使用invokedynamic指令(Java 7 ,即動態啟用)動態綁定該方法。

 

四.Lambda表達式在Java8中的運行機制

 

Lambda表達式的類型是一些類似於Comparator的接口。但並不是每個接口都可以使用Lambda表達式,只有那些僅僅包含一個非實例化抽象方法的接口才能使用Lambda表達式。這樣的接口被稱為函數式接口。並且它們能夠被@FunctionalInterface注解注釋。Runnable接口就是一個典型的函數式接口的。

 

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

 

@FunctionalInterface注解不是必須的,但它能夠讓工具知道這一個接口是一個函數式接口並表現有意義的行為。

eg:若試著編譯一個用@FunctionalInterface注釋自己並且含有多個抽象方法的接口,編譯就會報錯Multiple non-overriding abstract methods found

同樣的,若給一個不含有任何方法的接口添加@FunctionalInterface注解,會報錯No target method found.

 

 

Lambda表達式是采用動態啟用(Java7)來延遲在運行時的加載策略。當javac編譯代碼時,它會捕獲代碼中的Lambda表達式並且生成一個動態啟用的調用地址(稱為Lambda工廠)。當動態啟用被調用時,就會向Lambda表達式發生轉換的地方返回一個函數式接口的實例。然後將Lambda表達式的內容轉換到一個將會通過動態啟用來調用的方法中。在這一步驟中,JVM實現者有自由選擇策略的權利。

 

 

五.Lambda 作用域

在lambda表達式中訪問外層作用域和舊版本的匿名對象中的方式很相似。可以直接訪問標記了final的外層局部變量,或者實例的字段以及靜態變量。

欲看詳情請點擊:Lambda 作用域

(注:鑒於此篇博客文字篇幅過長,容易視覺疲勞,第一次學習也容易被嚇到等等原因,故將此詳情內容單獨寫了篇博客。)

 

六.捕獲和非捕獲的Lambda表達式

在lambda表達式中,可以訪問其外層作用域定義的變量。
eg:lambda表達式可以使用其外層定義的實例或靜態變量。lambda表達式也可以顯示或隱式地訪問this變量,該變量引用lambda表達式的外層類的調用的實例。因此,lambda表達式可以獲取或設置其外層類的實例或靜態變量的值,以及調用其外層類定義的方法。

 

Lambda表達式是否是捕獲的和性能相關。一個非不捕獲的Lambda通常比捕獲的更高效,雖然這一點目前沒有書面的規范說明(據我所知),而且也不能為了程序的正確性指望它做什麼,非捕獲的Lambda只需要計算一次. 然後每次使用到它都會返回一個唯一的實例。而捕獲的lambda表達式每次使用時都需要重新計算一次,而且從目前實現來看,它非常像實例化一個匿名內部類的實例。

 

當Lambda表達式訪問一個定義在Lambda表達式體外的非靜態變量或者對象時,這個Lambda表達式稱為“捕獲的”。

當lambda表達式使用其外層作用域定義的局部變量時,會產生一種特殊的情況,稱為變量捕獲。在這種情況下,lambda表達式只能使用實質上final的局部變量。實質上final的變量是指在第一次賦值以後,值不再發生變化的變量(即在後面的程序中沒有改變變量的值)。沒有必要顯示地將這種變量聲明為final,不過聲明為final也不是錯誤(Note:外層作用域的this參數自動是實質上final變量,lambda表達式沒有自己的this參數)。

eg:下面這個Lambda表達式捕捉了變量x

int x = 5; return y -> x + y;

為了保證這個Lambda表達式聲明是正確的,被它捕獲的變量必須是“有效final”的。所以要麼它們需要用final修飾符號標記,要麼保證它們在賦值後不能被改變。

 

Lambda表達式不能修改外層作用域內的局部變量。因為修改局部變量會移除其實質上的final狀態,從而使捕獲該變量變得不合法。

eg:演示實質上的final的局部變量和可變局部變量的區別

 

interface MyFunc {
    int func(int n);
}
class VarCapture {
    int num = 10;
    MyFunc mylambda = (n) ->
    {
        //這裡num的使用是正確的,它不修改num變量。
        int v = num + n;
        //但是,下面的num表達式是非法的,因為它試圖修改num的值。
        //  num++;
        return v;
    };
    //下面的也會導致一個錯誤,因為這將消除num的有效的final狀態
    //  num = 9;
}
分析:

 

num實質是final變量,所有可以在mylambda內使用。但是,如果修改了num,不管是在lambda表達式內還是表達式外,num就會丟失其實質上final的狀態。這會導致發生錯誤,程序將無法通過編譯。

Note:lambda表達式可以使用和修改調用其調用類的實例變量,只是不能使用其外層作用域內的局部變量,除非該變量實質上是final變量

 

七.Lambdas不能做的事

記住:有一些Lambdas不提供的特性。為了Java 8,它們被考慮到了,但是沒有被包括進去,由於簡化以及時間限制的原因。

1.Non-final* 變量捕獲

如果一個變量被賦予新的數值,它將不能被用於lambda之中。"final"關鍵字不是必需的,但變量必須是“有效final”的(上面討論過)。下面代碼不會被編譯:

eg:

 

int count = 0;
List strings = Arrays.asList("a", "b", "c");
strings.forEach(s -> {
    count++; // error: 不能修改count值
});

 

2.例外的透明度

如果一個已檢測的例外可能從Lambda內部拋出,功能性的接口也必須聲明已檢測例外可以被拋出。這種例外不會散布到其包含的方法。下面代碼不會被編譯:

eg:

 

    void appendAll(Iterable values, Appendable out) throws IOException { // doesn't help with the error
        values.forEach(s -> {
            out.append(s); // error: can't throw IOException here 
            // Consumer.accept(T) doesn't allow it
        });
    }

 

有繞過這個的辦法,可以定義自己的功能性接口,擴展Consumer的同時通過像RuntimeException類拋出 IOException。

3.控制流程 (break, early return)

在上面的 forEach例子中,傳統的繼續方式有可能通過在Lambda之內放置 "return;"來實現。但是,沒有辦法中斷循環或者從Lambda中通過包含方法的結果返回一個數值。

eg:

 

final String secret = "foo"; 
boolean containsSecret(Iterable values) {
    values.forEach(s -> { 
		if (secret.equals(s)) {
            ??? // want to end the loop and return true, but can't 
			}
    });
}

 

 

八.函數式通用接口的出現

 

想使用 Lambda 表達式,需要定義一個函數式接口,這樣往往會讓程序充斥著過量的僅為 Lambda 表達式服務的函數式接口。為了減少這樣過量的函數式接口,Java 8 在 java.util.function 中增加了不少新的函數式通用接口,有UnaryOperator,BinaryOperator,Consumer,Supplier,Function,Predicate等等。

 

1.Predicate :將 T 作為輸入。用來定義對一些條件的檢查。Predicate接口有一個叫test的方法,它需要一個T類型的值,返回值為布爾類型。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(與、或、非)。

eg:在一個names列表中找出所有以a開頭的name,可以這樣使用Predicate。

 

Predicate nameWithA= name -> name.startsWith("a");

 

2.Consumer:將 T 作為輸入,不返回任何內容,表示在單個參數上的操作。

 

用於表現那些不需要產生任何輸出的行為。Consumer接口中有一個叫做accept的方法,它需要一個T類型的參數並且沒有返回值。

 

eg:輸出指定信息

 

Consumer messageConsumer = message ->System.out.println(message);

 

3.Function:將 T 作為輸入,返回 R 作為輸出,它還包含了與其他函數組合的默認方法。需要一個值並返回一個結果。

 

eg:如果需要將所有names列表中的name轉換為大寫,可以像下面這樣寫一個Function:

 

Function toUpperCase = name -> name.toUpperCase();

 

 

4.Supplier: 這個函數式接口不需要傳值,但是會返回一個值。

 

eg: 用來生成唯一的標識符

 

Supplier uuidGenerator= () -> UUID.randomUUID().toString();

 

5.BinaryOperator:兩個T作為輸入,返回一個T作為輸出,對於“reduce”操作很有用

 

這些最原始的特征同樣存在。他們以int,long和double的方式提供。

 

eg:IntConsumer-以int作為輸入,執行某種動作,沒有返回值

 

這裡存在性能上的一些原因,主要是在輸入或輸出的時候避免裝箱和拆箱操作。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved