天天看點

Lambda表達式與函數式接口

Lambda 表達式是在Java 8中引入的,并且成為了Java 8亮點。它使得功能性程式設計變得非常便利,極大地簡化了開發工作。

讓我們從最簡單的例子開始,來學習如何對一個string清單進行排序。我們首先使用Java 8之前的方法來實作:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
 
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});
           

靜态工具方法Collections.sort接受一個list,和一個Comparator接口作為輸入參數,Comparator的實作類可以對輸入的list中的元素進行比較。通常情況下,你可以直接用建立匿名Comparator對象,并把它作為參數傳遞給sort方法。

除了建立匿名對象以外,Java 8 還提供了一種更簡潔的方式,Lambda表達式。

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});
           

你可以看到,這段代碼就比之前的更加簡短和易讀。但是,它還可以更加簡短:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

隻要一行代碼,包含了方法體。你甚至可以連大括号對{}和return關鍵字都省略不要。不過這還不是最短的寫法:

Collections.sort(names, (a, b) -> b.compareTo(a));

Java編譯器能夠自動識别參數的類型,是以你就可以省略掉類型不寫。可以看出:相對于之前使用匿名内部類的方式,Java8的lambda表達式更精簡。

Lambda表達式用處

1、凡是有匿名内部類的地方,都可以用Lambda表達式簡化。

2、Java8 Stream集合之流式操作,方法參數均為Lambda表達式。

Lambda文法解析

我們在此抽象一下lambda表達式的一般文法:

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}
           

Lambda表達式的定義

Lambda表達式是:一段帶有輸入參數的可執行語句塊,它不用被綁定到一個辨別符上,即不需要指派給一個變量,并且它将來可能被調用。

上面的例子

(String a, String b) -> {return b.compareTo(a)}

其實就是一個lambda表達式,并且還可以簡寫,省略參數類型和return,是以就成為最後的精簡版:

(a, b) -> b.compareTo(a)

一個Lambda表達式具有下面這樣的文法特征。它由三個部分組成:

  • 第一部分為一個括号(),裡面用逗号分隔的參數清單,參數即函數式接口裡面方法的參數;
  • 第二部分為一個箭頭符号:->;
  • 第三部分為一個大括号{},裡面是多條語句構成的方法體,可以是表達式和代碼塊。

簡寫版本說明

  • 參數類型省略,編譯器都可以從上下文環境中推斷出lambda表達式的參數類型。
  • 當lambda表達式的參數個數隻有一個,可以省略小括号。
  • 當lambda表達式隻包含一條語句時,可以省略大括号、return和語句結尾的分号。
  • 如果沒有參數則隻需(),例如 Thread 中的 run 方法就沒有參數傳入,當它使用 Lambda 表達式後:

    Thread t = new Thread(() -> { System.out.println("Hello from a thread in run");

下面列舉了Lambda表達式的幾個最重要的特征:

● 可選的類型聲明:你不用去聲明參數的類型。編譯器可以從參數的值來推斷它是什麼類型。

● 可選的參數周圍的括号:你可以不用在括号内聲明單個參數。但是對于很多參數的情況,括号是必需的。

● 可選的大括号:如果表達式體裡面隻有一個語句,那麼你不必用大括号括起來。

● 可選的傳回關鍵字:如果表達式體隻有單個表達式用于值的傳回,那麼編譯器會自動完成這一步。若要訓示表達式來傳回某個值,則需要使用大括号。

函數式接口

Lambda表達式如何比對Java的類型系統?語言的設計者們思考了很多如何讓現有的功能和lambda表達式友好相容。于是就有了函數式接口這個概念。函數式接口是一種隻有一個方法的接口,函數式接口可以隐式地轉換成 Lambda 表達式。

每一個lambda都能夠通過一個特定的接口,與一個給定的類型進行比對。一個所謂的函數式接口必須要有且僅有一個抽象方法聲明。每個與之對應的lambda表達式必須要與抽象方法的聲明相比對。

函數式接口的重要屬性是:我們能夠使用 Lambda 執行個體化它們,Lambda 表達式讓你能夠将函數作為方法參數,或者将代碼作為資料對待。Lambda 表達式的引入給開發者帶來了不少優點:在 Java 8 之前,匿名内部類,監聽器和事件處理器的使用都顯得很冗長,代碼可讀性很差,Lambda 表達式的應用則使代碼變得更加緊湊,可讀性增強。

要使用 Lambda 表達式,需要定義一個函數式接口,這樣往往會讓程式充斥着過量的僅為 Lambda 表達式服務的函數式接口。為了減少這樣過量的函數式接口,Java 8 在 java.util.function 中增加了不少新的函數式通用接口,即内置函數式接口。

内置函數式接口

Predicates預言式接口

Predicate<T> :将 T 作為輸入,傳回一個布爾值作為輸出,該接口包含多種預設方法來将 Predicate 組合成其他複雜的邏輯(與、或、非)。

Predicate是一個布爾類型的函數接口,該函數隻有一個輸入參數。Predicate接口包含了多種預設方法,用于處理複雜的邏輯動詞(and, or,negate)

Predicate<String> predicate = (s) -> s.length() > 0;
 
predicate.test("foo");              // true
predicate.negate().test("foo");     // false
 
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
 
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
           

Functions功能式接口

Function<T, R>:将 T 作為輸入,傳回 R 作為輸出,他還包含了和其他函數組合的預設方法。

Function接口接收一個參數,并傳回單一的結果。預設方法可以将多個函數串在一起(compse, andThen)。

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
 
backToString.apply("123");     // "123"
           

Consumers

Consumer<T> :将 T 作為輸入,不傳回任何内容,表示在單個參數上的操作。

Consumer代表了在一個輸入參數上需要進行的操作。

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
           

Suppliers

Supplier接口産生一個給定類型的結果。與Function不同的是,Supplier沒有輸入參數。

Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person
           

Comparators

Comparator接口在早期的Java版本中非常著名。Java 8 為這個接口添加了不同的預設方法。

Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
 
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
 
comparator.compare(p1, p2);             // > 0
comparator.reversed().compare(p1, p2);  // < 0
           

Lambda表達式通路其外部變量

以前java的匿名内部類在通路外部變量的時候,外部變量必須用final修飾。在java8對這個限制做了優化,可以不用顯示使用final修飾,但是編譯器隐式當成final來處理。

可以得到以下結論:

● 可通路 static 修飾的成員變量,如果是 final static 修飾,不可再次指派,隻有 static 修飾可再次指派;

● 可通路表達式外層的 final 局部變量(不用聲明為 final,隐性具有 final 語義),不可再次指派。

Java 8接口的增強

Java 8 對接口做了進一步的增強。在接口中可以添加使用 default 關鍵字修飾的非抽象方法。還可以在接口中定義靜态方法。如今,接口看上去與抽象類的功能越來越類似了。

預設方法

Java 8 還允許我們給接口添加一個非抽象的方法實作,隻需要使用 default 關鍵字即可,這個特征又叫做擴充方法。在實作該接口時,該預設擴充方法在子類上可以直接使用,它的使用方式類似于抽象類中非抽象成員方法。但擴充方法不能夠重載 Object 中的方法。例如:toString、equals、 hashCode 不能在接口中被重載。

靜态方法

在接口中,還允許定義靜态的方法。接口中的靜态方法可以直接用接口來調用。