《Java8實戰》-第三章讀書筆記(Lambda表示式-01)

雷俠發表於2018-08-11

Lambda表示式

在《Java8實戰》中第三章主要講的是Lambda表示式,在上一章節的筆記中我們利用了行為引數化來因對不斷變化的需求,最後我們也使用到了Lambda,通過表示式為我們簡化了很多程式碼從而極大地提高了我們的效率。那我們就來更深入的瞭解一下如何使用Lambda表示式,讓我們的程式碼更加具有簡潔性和易讀性。

Lambda管中窺豹

什麼是Lambda表示式?簡單的來說,Lambda表示式是一個匿名函式,Lambda表示式基於數學中的λ演算得名,直接對應其中的Lambda抽象(lambda abstraction),是一個匿名函式,既沒有函式名的函式。Lambda表示式可以表示閉包(注意和數學傳統意義的不同)。你也可以理解為,簡潔的表示可傳遞的匿名函式的一種方式:它沒有名稱,但它有引數列表、函式主體、返回型別,可能還有一個可以丟擲異常的列表。

有時候,我們為了簡化程式碼而去使用匿名類,雖然匿名類能簡化一部分程式碼,但是看起來很囉嗦。為了更好的的提高開發的效率以及程式碼的簡潔性和可讀性,Java8推出了一個核心的新特性之一:Lambda表示式。

Java8之前,使用匿名類給蘋果排序的程式碼:

apples.sort(new Comparator<Apple>() {
    @Override
    public int compare(Apple o1, Apple o2) {
        return o1.getWeight().compareTo(o2.getWeight());
    }
});
複製程式碼

是的,這段程式碼看上去並不是那麼的清晰明瞭,使用Lambda表示式改進後:

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
複製程式碼

或者是:

Comparator<Apple> byWeight = Comparator.comparing(Apple::getWeight);
複製程式碼

不得不承認,程式碼看起來跟清晰了。要是你覺得Lambda表示式看起來一頭霧水的話也沒關係,我們慢慢的來了解它。

現在,我們來看看幾個Java8中有效的Lambda表示式加深對Lambda表示式的理解:


// 這個表示式具有一個String型別的引數並返回一個int,Lambda並沒有return語句,因為已經隱含了return。
(String s) -> s.length() 

// 這個表示式有一個Apple型別的引數並返回一個boolean(蘋果重來是否大於150克)
(Apple a) -> a.getWeight() > 150

// 這個表示式具有兩個int型別二的引數並且沒有返回值。Lambda表示式可以包含多行程式碼,不只是這兩行。
(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x + y);
}

// 這個表示式沒有引數型別,返回一個int。
() -> 250

// 顯式的指定為Apple型別,並對重量進行比較返回int
(Apple a2, Apple a2) -> a1.getWeight.compareTo(a2.getWeight())
複製程式碼

Java語言設計者選選擇了這樣的語法,是因為C#和Scala等語言中的類似功能廣受歡迎。Lambda的基本語法是:

(parameters) -> expression
複製程式碼

或者(請注意花括號):

(parameters) -> {statements;}
複製程式碼

是的,Lambda表示式的語法看起來就是那麼簡單。那我們繼續看幾個例子,看看以下哪幾個是有效的:

(1) () -> {}
(2) () -> "Jack"
(3) () -> {return "Jack"}
(4) (Interge i) -> return "Alan" + i;
(5) (String s) -> {"IronMan";}
複製程式碼

正確答案是:(1)、(2)、(3)

原因:

(1) 是一個無參並且無返回的,類似與private void run() {}.

(2) 是一個無參並且返回的是一個字串。

(3) 是一個無參,並且返回的是一個字串,不過裡面還可以繼續寫一些其他的程式碼(利用顯式返回語句)。

(4) 它沒有使用使用顯式返回語句,所以它不能算是一個表示式。想要有效就必須加一對花括號, (Interge i) -> {return "Alan" + i}

(5) "IronMan"很顯然是一個表示式,不是一個語句,去掉這一對花括號或者使用顯式返回語句即可有效。

在哪裡以及如何使用Lambda

我們剛剛已經看了很多關於Lambda表示式的語法例子,可能你還不太清楚這個Lambda表示式到底如何使用。

還記得在上一章的讀書筆記中,實現的filter方法中,我們使用的就是Lambda:

List<Apple> heavyApples = filter(apples, (Apple apple) -> apple.getWeight() > 150);
複製程式碼

我們可以在函式式介面上使用Lambda表示式,函式式介面聽起來很抽象,但是不用太擔心接下來就會解釋函式式介面是什麼。

函式式介面

還記得第二章中的讀書筆記,為了引數化filter方法的行為使用的Predicate介面嗎?它就是一個函式式介面。什麼是函式式介面?一言蔽之,函式式介面就是隻定義了一個抽象方法的介面。例如JavaAPI中的:Comparator、Runnable、Callable:

public interface Comparable<T> {
    public int compareTo(T o);
}

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

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

複製程式碼

當然,不只是它們,還有很多一些其他的函式式介面。

函式式介面到底可以用來幹什麼?Lambda表示式允許你直接以內聯的形式為函式式介面的抽象方法提供例項,並把整個表示式作為函式式介面的例項(具體來說,是函式式介面一個具體實現的例項)。你也可以使用匿名類實現,只不過看來並不是那麼的一目瞭然。使用匿名類你需要提供一個例項,然後在直接內聯將它例項化。

通過下面的程式碼,你可以來比較一下使用函式式介面和使用匿名類的區別:

// 使用Lambda表示式
Runnable r1 = () -> System.out.println("HelloWorld 1");

// 使用匿名類
Runnable r2 = new Runnable() {
    @Override
    public void run() {
        System.out.println("HelloWorld 2");
    }
};

// 執行結果
System.out.println("Runnable執行結果:");
// HelloWorld 1
process(r1);
// HelloWorld 2
process(r2);
// HelloWorld 3
process(() -> System.out.println("HelloWorld 3"));
        
private static void process(Runnable r) {
    r.run();
}
複製程式碼

酷,從上面的程式碼可以看出使用Lambda表示式你可以減少很多程式碼同時也提高了程式碼的可讀性而使用匿名類卻要四五行左右的程式碼。

函式描述符

函式介面的抽象方法的前面基本上就是Lambda表示式的簽名。我們將這種抽象方法叫做函式描述符。例如,Runnable介面可以看作一個什麼也不接受什麼也不返回的函式簽名,因為它只有一個叫做run的抽象方法,這個方法沒有引數並且是無返回的。

使用函式式介面

函式式介面很有用,因為抽象方法的簽名可以描述Lambda表示式的簽名。函式式介面的抽象方法的簽名稱為函式描述符。

Predicate

在第一章的讀書筆記中,有提到過Predicate這個介面,現在我們來詳細的瞭解一下它。

java.util.function.Predicate介面定義了一個名字叫test的抽象方法,它接受泛型T物件,並返回一個boolean值。之前我們是建立了一個Predicate這樣的一個介面,現在我們所說到的這個介面和之前建立的一樣,現在我們不需要再去建立一個這樣的介面就直接可以使用了。在你需要表示一個涉及型別T的布林表示式時,就可以使用這個介面。比如,你可以定義一個接受String物件的Lambda表示式:

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

private static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> result = new ArrayList<>();
    for (T t : list) {
        if (predicate.test(t)) {
            result.add(t);
        }
    }
    return result;
}

List<String> strings = Arrays.asList("Hello", "", "Java8", "", "In", "Action");
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();

List<String> stringList = filter(strings, nonEmptyStringPredicate);
// [Hello, Java8, In, Action]
System.out.println(stringList);
複製程式碼

如果,你去檢視Predicate這個介面的原始碼你會發現有一些and或者or等等一些其他的方法,並且這個方法還有方法體,不過你目前無需關注這樣的方法,以後的文章將會介紹到為什麼在介面中能定義有方法體的方法。

Consumer

java.util.function.Consumer定義了一個叫做accept的抽象方法,它接受泛型T的物件,並且是一個無返回的方法。你如果需要訪問型別T的物件,並對其執行某些操作,就可以使用這個介面。比如,你可以用它來建立一個foreach方法,並配合Lambda來列印列表中的所有元素.

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

private static <T> void forEach(List<T> list, Consumer<T> consumer) {
    for (T i : list) {
        consumer.accept(i);
    }
}

// 使用Consumer
forEach(Arrays.asList("Object", "Not", "Found"), (String str) -> System.out.println(str));
forEach(Arrays.asList(1, 2, 3, 4, 5, 6), System.out::println);
複製程式碼

Function

java.util.function.Function<T, R>介面定義了一個叫做apply的方法,它接受一個泛型T的物件,並返回一個泛型R的物件。如果你需要定義一個Lambda,將輸入物件的資訊對映到輸出,就可以使用這個介面(比如提取蘋果的重量,把字串對映為它的長度)。在下面的程式碼中,我們來看看如何利用它來建立一個map方法,將以一個String列表對映到包含每個String長度的Integer列表。

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

private static <T, R> List<R> map(List<T> list, Function<T, R> function) {
    List<R> result = new ArrayList<>();
    for (T s : list) {
        result.add(function.apply(s));
    }
    return result;
}

 List<Integer> map = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length());
// [7, 2, 6]
System.out.println(map);
複製程式碼
原始型別特化

我們剛剛瞭解了三個泛型函式式介面:Predicate、Consumer和Function<T, R>。還有些函式式介面專為某些類而設計。

回顧一下:Java型別要麼用引用型別(比如:Byte、Integer、Object、List),要麼是原始型別(比如:int、double、byte、char)。但是泛型(比如Consumer中的T)只能繫結到引用型別。這是由泛型介面內部實現方式造成的。因此,在Java裡面有一個將原始型別轉為對應的引用型別的機制。這個機制叫作裝箱(boxing)。相反的操作,也就是將引用型別轉為對應的原始型別,叫作拆箱(unboxing)。Java還有一個自動裝箱機制來幫助程式設計師執行這一任務:裝箱和拆箱操作都是自動完成的。比如,這就是為什麼下面的程式碼是有效的(一個int被裝箱成為Integer):

List<Integer> list = new ArrayList<>;
for (int i = 0; i < 100; i++) {
    list.add(i);
}
複製程式碼

但是像這種自動裝箱和拆箱的操作,效能方面是要付出一些代價的。裝箱的本質就是將原來的原始型別包起來,並儲存在堆裡。因此,裝箱後的值需要更多的記憶體,並需要額外的記憶體搜尋來獲取被包裹的原始值。

Java8為我們前面所說的函式式介面帶來了一個專門的版本,以便在輸入和輸出都是原始型別時,避免自動裝箱的操作。比如,在下面的程式碼中,使用IntPredicate就避免了對值1000進行裝箱操作,但要使用Predicate就會把引數1000裝箱到一個Integer物件中:

@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
}

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
// 無裝箱
evenNumbers.test(1000);

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
// 裝箱
oddNumbers.test(1000);
複製程式碼

一般來說,針對專門的輸入引數型別的函式式介面的名稱都要加上對應的原始型別字首,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function介面還有針對輸出引數型別變種:ToIntFunction、IntToDoubleFunction等。

Java8中還有很多常用的函式式介面,如果你有興趣可以去查詢一些相關的資料,瞭解了這些常用的函式介面之後,會對你以後瞭解Stream的知識有很大的幫助。

《Java8實戰》這本書第三章的內容很多,所以我打算分兩篇文章來寫。這些讀書筆記系列的文章內容很多地方都是借鑑書中的內容。如果您有時間、興趣和經濟的話可以去買這本書籍。這本書我看了兩遍,是一本很不錯的技術書籍。如果,您沒有太多的時間那麼您就可以關注我的微信公眾號或者當前的技術社群的賬號,利用空閒的時間看看我的文章,非常感謝您對我的關注!

程式碼示例:

Github:chap3 Gitee: chap3

公眾號

《Java8實戰》-第三章讀書筆記(Lambda表示式-01)

相關文章