Java面向對象系列[Lambda]

Lambda表達式支持將代碼塊作爲方法參數,Lambda表達式允許使用更簡潔的代碼來創建只有一個抽象方法的接口(這種接口叫函數式接口)的實例

public class ProcessArray
{
    public void process(int[] target, Command cmd)
    {
        for (var t : target)
        {
            cmd.process(t);
        }
    }
}
public class CommandTest
{
    public static void main(String[] args)
    {
        var pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        // 處理數組,具體處理行爲取決於匿名內部類
        pa.process(target, new Command()
        {
            public void process(int[] target)
            {
                int sum = 0;
                for (int tmp : target)
                {
                    sum += tmp;
                }
                System.out.println("數組元素的總和是:" + sum);
            }
        });
    }
}

ProcessArray類的process()方法處理數組時,希望傳入一段代碼作爲具體的處理行爲,從代碼中可以看到,用於封裝處理行爲的關鍵就是代碼中的粗體部分,但爲了向process()方法傳入這段粗體代碼段,程序使用了匿名內部類的語法來創建對象

Lambda表達式簡化創建匿名內部類對象

當使用Lambda表達式代替匿名內部類創建對象時,Lambda表達式的代碼塊將會代替實現抽象方法的方法體,Lambda表達式就相當於一個匿名方法

public class CommandTest2
{
    public static void main(String[] args)
    {
        var pa = new ProcessArray();
        int[] array = {3, -4, 6, 4};
        // 處理數組,具體處理行爲取決於匿名內部類
        pa.process(array, (int[] target)->{
                int sum = 0;
                for (int tmp : target)
                {
                    sum += tmp;
                }
                System.out.println("數組元素的平方是:" + sum);
            });
    }
}

另一組實例

public interface Command
{
    // 接口裏定義的process()方法用於封裝“處理行爲”
    void process(int element);
}
public class CommandTest
{
    public static void main(String[] args)
    {
        var pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        // 處理數組,具體處理行爲取決於匿名內部類
        pa.process(target, new Command()
        {
            public void process(int element)
            {
                System.out.println("數組元素的平方是:" + element * element);
            }
        });
    }
}
public class CommandTest2
{
    public static void main(String[] args)
    {
        var pa = new ProcessArray();
        int[] array = {3, -4, 6, 4};
        // 處理數組,具體處理行爲取決於匿名內部類
        pa.process(array, (int element)->{
                System.out.println("數組元素的平方是:" + element * element);
            });
    }
}

比較兩段代碼的不同,第二種實現不需要new Xxx(){}這種繁瑣的代碼,不需要指出重寫的方法名字,不需要給出重寫的方法的返回值類型,只要給出重寫的方法括號以及括號裏的形參列表即可

Lambda表達式組成

  • 形參列表,形參列表允許省略形參類型,如果形參列表中只有一個參數,甚至連形參列表的圓括號都可以省略
  • 箭頭(->),必須使用英文中線和大於號
  • 代碼塊,如果代碼塊只包含一條語句,Lambda表達式允許省略代碼塊的花括號,那麼這條語句就不要用花括號表示語句結束,如果Lambda代碼塊只有一條return語句,甚至連return關鍵都可以省略,Lambda表達式需要返回值,而他的代碼塊中僅有一條省略了return的語句,Lambda表達式會自動返回這條語句的值。
interface Eatable
{
    void taste();
}
interface Flyable
{
    void fly(String weather);
}
interface Addable
{
    int add(int a, int b);
}
public class LambdaQs
{
    // 調用該方法需要Eatable對象
    public void eat(Eatable e)
    {
        System.out.println(e);
        e.taste();
    }
    // 調用該方法需要Flyable對象
    public void drive(Flyable f)
    {
        System.out.println("我正在駕駛:" + f);
        f.fly("我醉欲眠卿且去");
    }
    // 調用該方法需要Addable對象
    public void test(Addable add)
    {
        System.out.println("5與3的和爲:" + add.add(5, 3));
    }
    public static void main(String[] args)
    {
        var lq = new LambdaQs();
        // Lambda表達式的代碼塊只有一條語句,可以省略花括號。
        lq.eat(() -> System.out.println("蘋果的味道不錯!"));
        // Lambda表達式的形參列表只有一個形參,省略圓括號
        lq.drive(weather -> {
            System.out.println("今天天氣是:" + weather);
            System.out.println("直升機飛行平穩");
        });
        // Lambda表達式的代碼塊只有一條語句,省略花括號
        // 代碼塊中只有一條語句,即使該表達式需要返回值,也可以省略return關鍵字。
        lq.test((a, b) -> a + b);
    }
}
  • 代碼lq.eat(*()** -> System.out.println("蘋果的味道不錯!"**)*);調用eat()方法,調用該方法需要一個Eatable類型的參數,但實際傳入的是Lambda表達式

  • 代碼lq.drive(*weather -> { System.out.println("今天天氣是:" + weather); System.out.println("直升機飛行平穩"); **}*);調用drive()方法,調用該方法需要一個Flyable類型的參數,但實際傳入的是Lambda表達式

  • 代碼lq.test(*(**a, b) -> a + b*);調用test()方法,帶哦用該方法需要一個Addable類型的參數,但實際傳入的是Lambda表達式

  • 這說明Lambda表達式實際上將會被當成“任意類型”的對象,到底當成什麼類型的對象,則取決於運行環境的需要

Lambda表達式與函數式接口

Lambda表達式的“類型”,也被稱爲“目標類型(target type)”,Lambda表達式的目標類型必須是“函數式接口”,函數式接口代表只包含一個抽象方法的接口,它可以包含多個默認方法、類方法但只能聲明一個抽象方法。
如果採用匿名內部類語法來創建函數式接口的實例,則只需要實現一個抽象方法,在這種情況下即可採用Lambda表達式來創建對象,該表達式創建出來的對象的目標類型就是這個函數式接口。

Java8專門提供了函數式接口的註解@FunctionalInterface,該註解方法接口定義前面,雖然它對程序功能沒有任何作用,但可以告訴編譯器嚴格檢查它標註的接口必須是函數式接口,否則報異常

由於Lambda表達式的結果就是被當成對象,因此程序中完全可以使用Lambda表達式進行賦值

@FunctionalInterface
interface FkTest
{
    void run();
}

public class LambdaTest
{
    public static void main(String[] args)
    {
        // Runnable接口是Java本身提供的一個函數式接口,它只包含一個無參數的方法
        // Lambda表達式代表的匿名方法實現了Runnable接口中唯一的、無參數的方法
        // 因此下面的Lambda表達式創建了一個Runnable對象
        Runnable r = () -> {
            for (var i = 0; i < 100; i++)
            {
                System.out.println(i);
            }
        };
        /* 下面代碼報錯: 不兼容的類型: Object不是函數接口
        Object obj = () -> {
            for (var i = 0; i < 100; i++)
            {
                System.out.println(i);
            }
        };*/
        // 對Lambda表達式執行強制類型轉換,這樣可以確定該表達式的目標類型爲Runnable函數式接口
        Object obj1 = (Runnable)() -> {
            for (var i = 0; i < 100; i++)
            {
                System.out.println(i);
            }
        };

        // 同樣的Lambda表達式可以被當成不同的目標類型,唯一的要求是:
        // Lambda表達式的形參列表與函數式接口中唯一的抽象方法的形參列表相同
        // 前面強制轉型爲Runnable的Lambda表達式也可強轉爲FkTest類型,因爲FkTest接口中的
        // 唯一抽象方法是不帶參數的,而該Lambda表達式也是不帶參數的
        Object obj2 = (FkTest)() -> {
            for (var i = 0; i < 100; i++)
            {
                System.out.println();
            }
        };

    }
}

爲了保證Lambda表達式的目標類型是一個明確的函數式接口,有三種方式:

  • 將Lambda表達式賦值給函數式接口類型的變量
  • 將Lambda表達式作爲函數式接口類型的參數傳給某個方法
  • 使用函數式接口對Lambda表達式進行強制類型轉換

Java8在java.util.function包下預定義了大量函數式接口,例如XxxFunction、XxxConsumer、XxxxPredicate、XxxSupplier等等

方法引用和構造器引用

如果Lambda表達式的代碼塊只有一條代碼,程序就可以省略Lambda表達式中代碼塊的花括號,不僅如此,如果Lambda表達式的代碼塊只有一條代碼,還可以在代碼塊中使用方法引用和構造器引用,共支持4中引用

種類 示例 說明 對應的lambda表達式
引用類方法 類名 ::類方法 函數式接口中被實現方法的全部參數傳給該類方法作爲參數 (a,b…) -> 類名.類方法(a,b,…)
引用特定對象的實例方法 特定對象::實例方法 函數式接口中被實現方法的全部參數傳給該方法作爲參數 (a,b…) -> 特定對象.實例方法(a,b,…)
引用某類對象的實例方法 類名::實例方法 函數式接口中被實現方法的第一個參數作爲調用者,後面的參數全部傳給該方法作爲參數 (a,b,c,…) -> a.實例方法(b,c,…)
引用構造器 類名::new 函數式接口中被實現方法的全部參數傳給該構造器作爲參數 (a,b,…) -> new 類名(a,b,…)

引用類方法

@FunctionalInterface
interface Converter{
    Integer convert(String from);
}
public class MethodRefer
{
    public static void main(String[] args)
    {
        // 下面代碼使用Lambda表達式創建Converter對象
        // Lambda表達式的代碼塊只有一條語句,因此省略了花括號
        // 由於表達式所實現的convert()方法需要返回值,因此Lambda表達式將會把這條語句的結果作爲返回值
        Converter converter1 = from -> Integer.valueOf(from);
        // 方法引用代替Lambda表達式:引用類方法
        // 函數式接口中被實現方法的全部參數傳給該類方法作爲參數
        Converter converter1 = Integer::valueOf;
        Integer val = converter1.convert("99");
        System.out.println(val); // 輸出整數99
        /* Lambda表達式的代碼塊只有一行調用類方法的代碼,將其替換成了方法引用
      * 類方法引用,也就是調用Integer類的valueOf()類方法來實現Converter函數式接口中唯一的抽象方法
      * 當調用Converter接口中唯一的抽象方法時,調用參數將會傳給Interger類的valueOf()類方法
         */
    }
}

引用特定對象的實例方法

@FunctionalInterface
interface Converter{
    Integer convert(String from);
}
public class MethodRefer
{
    public static void main(String[] args)
    {
        // 下面代碼使用Lambda表達式創建Converter對象
        Converter converter2 = from -> "fkit.org".indexOf(from);
        // 方法引用代替Lambda表達式:引用特定對象的實例方法。
        // 函數式接口中被實現方法的全部參數傳給該方法作爲參數。
        Converter converter2 = "fkit.org"::indexOf;
        Integer value = converter2.convert("it");
        System.out.println(value); // 輸出2
    }
}

引用某類對象的實例方法

@FunctionalInterface
interface MyTest 
{
    String test(String a, int b, int c);
}

public class MethodRefer
{
    public static void main(String[] args)
    {
        // 下面代碼使用Lambda表達式創建MyTest對象
        MyTest mt = (a, b, c) -> a.substring(b, c);
        // 方法引用代替Lambda表達式:引用某類對象的實例方法。
        // 函數式接口中被實現方法的第一個參數作爲調用者,
        // 後面的參數全部傳給該方法作爲參數。
        MyTest mt = String::substring;
        String str = mt.test("Java I Love you", 2, 9);
        System.out.println(str); // 輸出:va I Lo
    }
}

引用構造器

@FunctionalInterface
interface YourTest
{
    JFrame win(String title);
}
public class MethodRefer
{
    public static void main(String[] args)
    {
        // 下面代碼使用Lambda表達式創建YourTest對象
        YourTest yt = a -> new JFrame(a);
        // 構造器引用代替Lambda表達式。
        // 函數式接口中被實現方法的全部參數傳給該構造器作爲參數。
        YourTest yt = JFrame::new;
        JFrame jf = yt.win("我的窗口");
        System.out.println(jf);
    }
}
  • 調用JFrame類的構造器來實現YourTest函數式接口中唯一的抽象方法,當調用YourTest接口中唯一的抽象方法時,調用參數將會傳給JFrame構造器
  • 調用YourTest對象的win()抽象方法時,實際傳入了一個String類型的參數,這個參數將會被傳給JFrame構造器,因此是調用了JFrame類的帶一個String參數的構造器

Lambda表達式與匿名函數內部類的聯繫【可以忽略】

Lambda表達式是匿名內部類的一種簡化,因此它可以部分取代匿名內部類的作用,Lambda表達式與匿名內部類的共同點:

  • Lambda表達式與匿名內部類一樣,都可以直接訪問“effectively final”的局部變量,以及外部類的成員變量(包括實例變量和類變量)
  • Lambda表達式創建的對象和匿名內部類生成的對象一樣,都可以直接調用從接口中繼承的默認方法
@FunctionalInterface
interface Displayable
{
    // 定義一個抽象方法和默認方法
    void display();
    default int add(int a, int b)
    {
        return a + b;
    }
}
public class LambdaAndInner
{
    private int age = 12;
    private static String name = "我欲乘風歸去";
    public void test()
    {
        var book = "又恐瓊樓玉宇";
        Displayable dis = ()->{
            // 訪問“effectively final”的局部變量
            System.out.println("book局部變量爲:" + book);
            // 訪問外部類的實例變量和類變量
            System.out.println("外部類的age實例變量爲:" + age);
            System.out.println("外部類的name類變量爲:" + name);
            // 嘗試調用接口中的默認方法,編譯器會報錯
            // System.out.println(add(3, 5));
        };
        dis.display();
        // 調用dis對象從接口中繼承的add()方法
        System.out.println(dis.add(3, 5));    
    }
    public static void main(String[] args)
    {
        var lambda = new LambdaAndInner();
        lambda.test();
    }
}
  • Lambda表達式創建了一個Displayable的對象,Lambda表達式的代碼塊中的System.out.println("book局部變量爲:" + book); System.out.println("外部類的age實例變量爲:" + age); System.out.println("外部類的name類變量爲:" + name);代碼分別示範了訪問“effectively final”的局部變量、外部類的實例變量和類變量
  • Lambda表達式訪問了book局部變量,因此該局部變量相當於有一個隱式的final修飾,就不允許對book局部變量重新賦值
  • 代碼System.out.println(dis.add(3, 5));表明,Lambda表達式創建了Displayable的對象後,該對象不僅可以調用接口中唯一的抽象方法,也可以調用接口中的默認方法

Lambda表達式與匿名函數內部類的區別【可以忽略】

  • 匿名內部類可以爲任意接口創建實例,無論接口裏有多少個抽象方法,只要匿名內部類實現所有的抽象方法即可,但Lambda表達式只能爲函數式接口創建實例
  • 匿名內部類可以爲抽象類甚至普通類創建實例,但Lambda表達式不幸
  • 匿名內部類實現的抽象方法的方法體允許調用接口中定義的默認方法,但Lambda表達式的代碼塊不允許調用接口中定義的默認方法,因此 System.out.println(add(3, 5));會報錯,如果將Lambda表達式改爲匿名內部類的寫法,當匿名內部類實現display()抽象方法時,則完全可以調用add()方法

使用Lambda表達式調用Arrays的類方法

import java.util.Arrays;
public class LambdaArrays
{
    public static void main(String[] args)
    {
        var arr1 = new String[] {"java", "fkava", "fkit", "ios", "android"};
        Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
        System.out.println(Arrays.toString(arr1));
        var arr2 = new int[] {3, -4, 25, 16, 30, 18};
        // left代表數組中前一個所索引處的元素,計算第一個元素時,left爲1
        // right代表數組中當前索引處的元素
        Arrays.parallelPrefix(arr2, (left, right)-> left * right);
        System.out.println(Arrays.toString(arr2));
        var arr3 = new long[5];
        // operand代表正在計算的元素索引
        Arrays.parallelSetAll(arr3, operand -> operand * 5);
        System.out.println(Arrays.toString(arr3));
    }
}

Lambda表達式(o1, o2) -> o1.length() - o2.length()的目標類型是Comparator, 該函數式接口指定了判斷字符串大小的標準,字符串越長,即認爲該字符串越大
Lambda表達式(left, right)-> left * right的目標類型是IntBinaryOperator,該函數式接口根據前後兩個元素來計算當前元素的值
Lambda表達式operand -> operand * 5的目標類型是IntToLongFunction,該函數式接口根據元素的索引來計算當前元素的值
執行結果如下:

[ios, java, fkit, fkava, android]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章