不知道面試會不會問Lambda怎麼用

邊緣煩惱發表於2019-04-16

不知道面試會不會問Lambda怎麼用
我們先假設一個場景想象一下,當一個專案出現bug的時候,恰巧這個時候需要你去修改,而當你開啟專案之後,眼前的程式碼讓你有一種特別嚴重的陌生感,你會不會慌?心裡是不是瞬間就會噴湧而出各種想法:我這是開啟的啥語言的專案?還是我眼花看錯了?難道是我過時了?這寫的是個啥子玩意兒…

java8在14年就出來了,已經很久了,但是還是有很多人沒用過,包括我之前的同事都對這個不太熟悉,原因可能是多樣的,可能是老程式設計師覺得沒必要;也可能是性格使然,拒絕接受新的東西,一切守舊,能用就行;也可能是專案太老了,還在用JDK1.7,或者更老的版本,平時根本就接觸不到java8的寫法,也不需要去接觸。

無論是什麼原因,在新事物出現之後,沒有一股探險精神,不去嘗試,不去結合自己的處境去思考,這樣下去就算天上掉餡餅也輪不到你啊。 這篇短文說下Lambda表示式,有一定的程式設計基礎的小夥伴簡單看下應該就會明白,不僅僅寫著舒服,更能提供你的工作效率,讓你有更多的時間帶薪划水,自我提高,走向人生巔峰。

Lambda表示式

Lambda表示式可以理解為一種匿名函式:沒有名稱、有引數列表、函式主體、返回型別,可能還會有異常的列表。

引數 -> 主體

lambda表示式:(parameters) -> expression 或者是 (parameters) -> { statements; }

函式式介面

什麼是函式式介面?

僅僅定義了一個抽象方法的介面,類似於Predicate、Comparator和Runnable。 @FunctionalInterface 函式式介面都帶有這個註解,這個註解表示這個介面會被設計為函式式介面。

行為引數化

一個方法接受多個不同的行為作為引數,並在內部使用它們,完成不同行為的能力。

函式式介面可以做些什麼?

Lambda表示式允許你直接以內聯的形式為函式式介面的抽象方法提供實現,並且把整個表示式作為函式式介面的例項,也就是說,Lambda是函式式介面的一個具體實現。函式式介面和Lambda會在專案中寫出更加簡潔易懂的程式碼。 接下來我們看下幾種函式式介面:

  • java.util.function.Predicate:這個介面中定義了一個test的抽象方法,它接受泛型T物件,並返回一個boolean值,在你需要表示一個涉及型別T的布林表示式時,就可以使用這個介面。
  • java.util.function.Consumer:這個介面中定義了accept抽象方法,它接受泛型T的物件,沒有返回。如果你需要訪問型別T的物件,並執行某些操作,可以用它。
  • java.util.function.Function:這個介面定義了一個apply的方法,它接受一個泛型T的物件,並返回一個泛型R的物件,如果你需要定一個Lambda,將輸入物件的資訊對映到輸出物件,就可以使用這個介面。
  • ps:我們也可以自己定義一個自己需要的函式式介面。 這麼說實在是太生澀了,還是貼點程式碼,讓大家都看看:
@FunctionalInterface
public interface Predicate<T> {
    //我只擷取了部分程式碼,test是這個介面唯一的抽象方法,話說從java8開始,介面中不僅   
    //僅只能有抽象方法了,實現的方法也可以存在,用default和static來修飾。
    boolean test(T t);
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
複製程式碼

接下來,看下Lambda和函式式介面是怎麼配合,一起快樂的工作的: 首先定義一個方法,這個方法的引數中有函式式介面:

private static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> result = new ArrayList<>();
    for (T e : list) {
        if (predicate.test(e)) {
            result.add(e);
        }
    }
    return result;
}
複製程式碼

接下來你就可以這麼寫:

List<Apple> apples = filter(list, (Apple apple) -> "red".equals(apple.getColor()));
複製程式碼

以上,filter方法的引數是一個泛型集合和Predicate,這個函式式介面中的抽象方法是接受一個物件並返回一個布林值,所以Lambda我們可以寫成引數是一個實體物件Apple,主體是一個返回boolean值的表示式,將這段Lambda作為引數傳給filter()方法,這也是java8的行為引數化特性。以上我們就可以挑選出紅蘋果。 使用了泛型,就代表著我們還可以複用這段程式碼做些別的事情,挑選出你想要東東的:

List<String> stringList = filter(strList, StringUtils::isNoneBlank);
複製程式碼

抽象方法的方法簽名和Lambda表示式的簽名是一一對應的,如果你要應用不同的Lambda表示式,就需要多個函式式介面,當然了我也是可以自己定義的。

在java中只有引用型別或者是原始型別,這是由泛型內部的實現方式造成的。因此,在Java裡有一個將原始型別轉換為對應的引用型別的機制,這個機制叫作裝箱(boxing)。相反的操作,也就是將引用型別轉換為對應的原始型別,叫作拆箱(unboxing)。

Java還有一個自動裝箱機制,也就是說裝箱和拆箱操作是自動完成的,但這在效能方面是要付出代價的。裝箱後的值本質上就是把原始型別包裹起來,並儲存在堆裡。因此,裝箱後的值需要更多的記憶體,並需要額外的記憶體來搜尋獲取被包裹的原始值。

針對於這一點,java8中的函式式介面提供了單獨的介面,就是為了在輸入和輸出的時候避免自動裝箱拆箱的操作,是不是很貼心。

一般情況下,在名稱上我們就能看得出來,一目瞭然。在原來的名稱上會有原始型別字首。像Function介面針對輸出引數型別的變形。比如說:ToIntFunction、IntToDoubleFunction等。

在必要的情況下,我們也可以自己定義一個函式式介面,請記住,(T,U) -> R的表達方式展示了對一個函式的簡單描述,箭頭的的左側代表了引數型別,右側代表著返回型別,這裡它代表一個函式,具有兩個引數,分別為泛型T和U,返回型別為R。

函式式介面是不允許丟擲 受檢異常(checked exception),但是有兩個方法可以丟擲異常:

  • 定義一個自己的函式式介面,在唯一的抽象方法丟擲異常;
  • 用try-catch 將lambda 包起來。

型別檢查

java7是通過泛型從上下文推斷型別,lambda的型別檢查是通過它的上下文推斷出來的。lambda會找到它所在的方法的方法簽名,也就是它的引數,也就是他們說的目標型別,再找到這個方法中定義的抽象方法,這個方法描述的函式描述符是什麼?也就是這個方法是個什麼樣的,接受什麼引數,返回什麼。lambda也必須是符合這樣的。當lambda丟擲異常的時候,那個抽象方法也必須要丟擲異常。 有了目標型別,那麼同一個lambda就可以與不同的函式式介面聯絡起來。只要他們的抽象方法簽名是一樣的。 例如:

Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
複製程式碼

這兩個介面都是沒有引數,且返回一個泛型T的函式。 void相容規則 lambda的主題是一個語句表示式,和一個返回void的函式描述符相容,包括引數列表, 比如下面:

// Predicate返回了一個boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一個void
Consumer<String> b = s -> list.add(s);
複製程式碼

在lambda中使用區域性變數

final int local_value = 44;
Consumer<String> stringConsumer = (String s) -> {
            int new_local_value = s.length() + local_value;
        };
複製程式碼

在lambda中可以無限制的使用例項變數和靜態變數,但是隻能是final的,如果在表示式裡面給變數賦值,就會編譯不通過。為什麼會有這樣的呢? 因為例項變數儲存在堆中,區域性變數儲存在棧中,lambda是在一個執行緒中,如果lambda可以直接訪問區域性變數,lambda的執行緒可能會在分配該變數的執行緒將這個變數回收之後,再去訪問該變數。在訪問區域性變數的時候,實際上是訪問他的副本,而不是原始變數。

方法引用

方法引用,方法目標實體放在::的前面,方法名放在後面。比如 Apple::getWeight,不需要括號。 建構函式是可以利用它的名稱和關鍵字 new來建立一個引用。

//Supplier也是一個函式式介面,唯一的抽象方法不接受引數,直接返回一個物件
Supplier<Apple> sup = Apple::new;
        Apple apple = sup.get();
複製程式碼

但是如果是有引數的呢?

//一個引數
Function<Long, Apple> fun = Apple::new;
        Apple apple1 = fun.apply(110L);
//兩個引數
 BiFunction<Long, String, Apple> biFunction = Apple::new;
 Apple biApple = biFunction.apply(3L, "red");
複製程式碼

但是如果有三個引數、四個引數呢?我們上面說了怎麼樣可以自定義一個自己需要的函式式介面。

@FunctionalInterface
public interface AppleWithParam<T, U, V, R> {
    R apply(T t, U u, V v);
}
複製程式碼

總結:

  • java8中自帶的函式式介面,以及為了避免拆裝箱操作而產生的函式式介面的原始型別轉化。
  • 函式式介面就是僅僅定義一個抽象方法的介面。抽象方法的簽名(稱為函式描述符) 描述了Lambda表示式的簽名。
  • 只有在接受函式式介面的地方才可以使用Lambda表示式。
  • 介面現在還可以擁有預設方法,(就是類沒有對方法進行實現的時候,它實現的介面來提供預設實現的方法)

最後

只有主動擁抱變化,才能更快的成長。

如果對本文有任何異議或者說有什麼好的建議,可以加我好友(公眾號後臺聯絡作者),也可以在下面留言區留言。希望這篇文章能幫助大家披荊斬棘,乘風破浪。

這樣的分享我會一直持續,你的關注、轉發和好看是對我最大的支援,感謝。

不知道面試會不會問Lambda怎麼用

相關文章