天天看點

Comparator 以及 Lambda表達式

在數學中,函數就是有輸入量、輸出量的一套計算方案,也就是“拿什麼東西做什麼事情”。相對而言,面向對象過分強調“必須通過對象的形式來做事情”,而函數式思想則盡量忽略面向對象的複雜文法——強調做什麼,而不是以什麼形式做。

做什麼,而不是怎麼做

我們真的希望建立一個匿名内部類對象嗎?不。我們隻是為了做這件事情而不得不建立一個對象。我們真正希望做的事情是:将run方法體内的代碼傳遞給Thread類知曉。

傳遞一段代碼——這才是我們真正的目的。而建立對象隻是受限于面向對象文法而不得不采取的一種手段方式。那,有沒有更加簡單的辦法?如果我們将關注點從“怎麼做”回歸到“做什麼”的本質上,就會發現隻要能夠更好地達到目的,過程與形式其實并不重要。

當需要啟動一個線程去完成任務時,通常會通過java.lang.Runnable接口來定義任務内容,并使用java.lang.Thread類來啟動該線程。

傳統寫法,代碼如下:

public class Demo01ThreadNameless {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多線程任務執行!");
            }
        }).start();
    }
}public class Demo01ThreadNameless {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多線程任務執行!");
            }
        }).start();
    }
}
           

本着“一切皆對象”的思想,這種做法是無可厚非的:首先建立一個Runnable接口的匿名内部類對象來指定任務内容,再将其交給一個線程來啟動。

代碼分析:

對于Runnable的匿名内部類用法,可以分析出幾點内容:

Thread類需要Runnable接口作為參數,其中的抽象run方法是用來指定線程任務内容的核心:

  • 1.為了指定run的方法體,不得不需要Runnable接口的實作類;
  • 2.為了省去定義一個RunnableImpl實作類的麻煩,不得不使用匿名内部類;

必須覆寫重寫抽象run方法,是以方法名稱、方法參數、方法傳回值不得不再寫一遍,且不能寫錯;

而實際上,似乎隻有方法體才是關鍵所在。

Lambda表達式寫法,代碼如下:

借助Java 8的全新文法,上述Runnable接口的匿名内部類寫法可以通過更簡單的Lambda表達式達到等效:

public class Demo02LambdaRunnable {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("多線程任務執行!")).start(); // 啟動線程
    }
}
           

這段代碼和剛才的執行效果是完全一樣的,可以在1.8或更高的編譯級别下通過。從代碼的語義中可以看出:我們啟動了一個線程,而線程任務的内容以一種更加簡潔的形式被指定。

不再有“不得不建立接口對象”的束縛,不再有“抽象方法覆寫重寫”的負擔,就是這麼簡單!

Lambda的格式

标準格式:

Lambda省去面向對象的條條框框,格式由3個部分組成:

一些參數

一個箭頭

一段代碼

Lambda表達式的标準格式為:

> (參數類型 參數名稱) -> { 代碼語句 } 格式說明:

小括号内的文法與傳統方法參數清單一緻:無參數則留白;多個參數則用逗号分隔。

->是新引入的文法格式,代表指向動作。

大括号内的文法與傳統方法體要求基本一緻。

匿名内部類與lambda對比:

new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多線程任務執行!");
            }
}).start();
           

仔細分析該代碼中,Runnable接口隻有一個run方法的定義:

public abstract void run();

即制定了一種做事情的方案(其實就是一個方法):

  • 無參數:不需要任何條件即可執行該方案。
  • 無傳回值:該方案不産生任何結果
  • 代碼塊(方法體):該方案的具體執行步驟。
  • 同樣的語義展現在Lambda文法中,要更加簡單:() -> System.out.println(“多線程任務執行!”)

    前面的一對小括号即run方法的參數(無),代表不需要任何條件;

中間的一個箭頭代表将前面的參數傳遞給後面的代碼;

後面的輸出語句即業務邏輯代碼。

參數和傳回值:

下面舉例示範java.util.Comparator接口的使用場景代碼,其中的抽象方法定義為:

當需要對一個對象數組進行排序時,Arrays.sort方法需要一個Comparator接口執行個體來指定排序的規則。假設有一個Person類,含有String name和int age兩個成員變量:

public class Person { 
    private String name;
    private int age;
    
    // 省略構造器、toString方法與Getter Setter 
}

           

傳統寫法

如果使用傳統的代碼對Person[]數組進行排序,寫法如下:

public class Demo06Comparator {
    public static void main(String[] args) {
        // 本來年齡亂序的對象數組
        Person[] array = { new Person("古力娜紮", 19),          new Person("迪麗熱巴", 18),             new Person("馬爾紮哈", 20) };
​
        // 匿名内部類
        Comparator<Person> comp = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        };
        Arrays.sort(array, comp); // 第二個參數為排序規則,即Comparator接口執行個體
​
        for (Person person : array) {
            System.out.println(person);
        }
    }
}
           

這種做法在面向對象的思想中,似乎也是“理所當然”的。其中Comparator接口的執行個體(使用了匿名内部類)代表了“按照年齡從小到大”的排序規則。

代碼分析

下面我們來搞清楚上述代碼真正要做什麼事情:

  1. 為了排序,Arrays.sort方法需要排序規則,即Comparator接口的執行個體,抽象方法compare是關鍵;
  2. 為了指定compare的方法體,不得不需要Comparator接口的實作類;
  3. 為了省去定義一個ComparatorImpl實作類的麻煩,不得不使用匿名内部類;

必須覆寫重寫抽象compare方法,是以方法名稱、方法參數、方法傳回值不得不再寫一遍,且不能寫錯;

實際上,隻有參數和方法體才是關鍵。

Lambda寫法

public class Demo07ComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
            new Person("古力娜紮", 19),
            new Person("迪麗熱巴", 18),
            new Person("馬爾紮哈", 20) };
​
        Arrays.sort(array, (Person a, Person b) -> {
            return a.getAge() - b.getAge();
        });
​
        for (Person person : array) {
            System.out.println(person);
        }
    }
}
           

省略格式:

省略規則

在Lambda标準格式的基礎上,使用省略寫法的規則為:

  1. 小括号内參數的類型可以省略;
  2. 如果小括号内有且僅有一個參,則小括号可以省略;
  3. 如果大括号内有且僅有一個語句,則無論是否有傳回值,都可以省略大括号、return關鍵字及語句分号。

備注:掌握這些省略規則後,請對應地回顧本章開頭的多線程案例。

可推導即可省略

Lambda強調的是“做什麼”而不是“怎麼做”,是以凡是可以根據上下文推導得知的資訊,都可以省略。例如上例還可以使用Lambda的省略寫法:

Runnable接口簡化:
1. () -> System.out.println("多線程任務執行!")
Comparator接口簡化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());
           

Lambda的前提條件

Lambda的文法非常簡潔,完全沒有面向對象複雜的束縛。但是使用時有幾個問題需要特别注意:

使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法。 無論是JDK内置的Runnable、Comparator接口還是自定義的接口,隻有當接口中的抽象方法存在且唯一時,才可以使用Lambda。

使用Lambda必須具有上下文推斷。 也就是方法的參數或局部變量類型必須為Lambda對應的接口類型,才能使用Lambda作為該接口的執行個體。

備注:有且僅有一個抽象方法的接口,稱為“函數式接口”。