Java 8 函数接口

歡迎來到Java 8功能介面範例教程。Java一直以來都是一種物件導向編程語言。這意味著Java編程的一切都圍繞著物件(除了一些簡單的原始類型)。在Java中,我們不僅有函數,它們是類的一部分,我們需要使用類/物件來調用任何函數。

Java 8功能介面

如果我們看一些其他的編程語言,比如C++、JavaScript;它們被稱為函數式編程語言,因為我們可以編寫函數並在需要時使用它們。這些語言中有一些同時支持物件導向編程和函數式編程。物件導向並不是壞事,但它給程序帶來了很多冗贅。例如,假設我們必須創建一個Runnable的實例。通常我們使用匿名類來做到這一點,就像下面這樣。

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

如果你看上面的代碼,實際有用的部分是run()方法內的代碼。其餘的代碼都是因為Java程序結構的方式。Java 8功能介面和Lambda表達式幫助我們通過刪除大量樣板代碼來編寫更小、更清潔的代碼。

Java 8功能接口

具有恰好一个抽象方法的接口称为功能接口。我们可以添加@FunctionalInterface注解来将接口标记为功能接口。虽然不是强制性的,但最好在功能接口中使用它,以避免意外添加额外的方法。如果接口使用@FunctionalInterface注解,并且我们试图拥有多个抽象方法,则会抛出编译器错误。Java 8功能接口的主要好处是我们可以使用lambda表达式来实例化它们,避免使用臃肿的匿名类实现。Java 8集合API已经重写,并引入了使用许多功能接口的新Stream API。Java 8在java.util.function包中定义了许多功能接口。一些有用的Java 8功能接口包括ConsumerSupplierFunctionPredicate。您可以在Java 8 Stream示例中找到有关它们的更多详细信息。java.lang.Runnable是具有单个抽象方法run()的功能接口的一个很好的示例。下面的代码片段提供了一些功能接口的指导:

interface Foo { boolean equals(Object obj); }
// 因為 equals 已經是一個隱式成員(Object 類別)而無法使用

interface Comparator {
 boolean equals(Object obj);
 int compare(T o1, T o2);
}
// 因為 Comparator 只有一個抽象的非 Object 方法,所以是有效的

interface Foo {
  int m();
  Object clone();
}
// 因為方法 Object.clone 不是公共的,所以無效

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// 有效:兩個方法,但具有相同的簽名

interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// 有效:Y.m 是一個子簽名且可以替換返回類型的

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// 無效:沒有任何方法具有所有抽象方法的子簽名

interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class c); }
interface Z extends X, Y {}
// 無效:沒有任何方法具有所有抽象方法的子簽名

interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// 編譯器錯誤:沒有方法是可以替換返回類型的

interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// 編譯器錯誤:不同的簽名,相同的擦除

Lambda 表達式

Lambda 表達式是我們可以在 Java 面向對象的世界中視覺化函數式編程的方式。對象是 Java 編程語言的基礎,我們永遠不能沒有一個函數而沒有一個對象,這就是為什麼 Java 語言僅支持使用函數式接口來使用 Lambda 表達式的原因。由於函數式接口中只有一個抽象函數,因此在應用 Lambda 表達式到該方法時不會產生混淆。Lambda 表達式的語法是(參數) -> (主體)。現在讓我們看看如何使用 Lambda 表達式來編寫上面的匿名 Runnable。

Runnable r1 = () -> System.out.println("My Runnable");

讓我們試著理解上面的 Lambda 表達式中發生了什麼。

  • Runnable 是一個函數介面,這就是為什麼我們可以使用 Lambda 表達式來創建它的實例。
  • 由於 run() 方法不接受任何參數,因此我們的 Lambda 表達式也沒有參數。
  • 就像 if-else 塊一樣,我們可以避免使用大括號({}),因為方法主體中只有一個語句。對於多個語句,我們將不得不像其他方法一樣使用大括號。

我們為什麼需要 Lambda 表達式

  1. 代碼行數減少使用 Lambda 表達式的一個明顯好處是代碼量減少,我們已經看到了如何使用 Lambda 表達式輕鬆創建函數介面的實例,而不是使用匿名類別。

  2. 順序和並行執行支援使用 lambda 表達式的另一個好處是我們可以從 Stream API 的順序和並行操作支援中受益。為了解釋這一點,讓我們舉一個簡單的例子,我們需要寫一個方法來測試傳遞的數字是否為質數。傳統上,我們會像下面這樣編寫它的代碼。代碼並非完全優化,但對於示例目的來說是好的,所以請容忍我。

    //傳統方法
    private static boolean isPrime(int number) {		
    	if(number < 2) return false;
    	for(int i=2; i<number; i++){
    		if(number % i == 0) return false;
    	}
    	return true;
    }
    

    上述代碼的問題在於它是順序性的,如果數字非常大,那麼它將需要大量的時間。代碼的另一個問題是有太多的退出點,並且不易讀取。讓我們看看如何使用 lambda 表達式和流 API 編寫相同的方法。

    //聲明式方法
    private static boolean isPrime(int number) {		
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					index -> number % index == 0);
    }
    

    IntStream 是支援順序和並行聚合操作的原始 int 值元素序列。這是 Stream 的 int 原始特化。為了更易讀,我們也可以像下面這樣編寫該方法。

    private static boolean isPrime(int number) {
    	IntPredicate isDivisible = index -> number % index == 0;
    	
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					isDivisible);
    }
    

    如果您不熟悉 IntStream,它的 range() 方法會返回一個從 startInclusive(包含)到 endExclusive(不包含)的順序有序 IntStream,增量步驟為 1。noneMatch() 方法返回此流的元素是否均不匹配提供的斷言。如果不需要確定結果,則可能不會對所有元素進行斷言評估。

  3. 將行為傳遞到方法 讓我們看看如何使用 lambda 表達式來通過一個簡單的例子傳遞方法的行為。假設我們需要編寫一個方法來對列表中的數字進行求和,如果它們符合給定的條件。我們可以使用 Predicate 並編寫如下方法。

    public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) {
    	    return numbers.parallelStream()
    	    		.filter(predicate)
    	    		.mapToInt(i -> i)
    	    		.sum();
    	}
    

    範例用法:

    //總和所有數字
    sumWithCondition(numbers, n -> true)
    //總和所有偶數
    sumWithCondition(numbers, i -> i%2==0)
    //總和所有大於5的數字
    sumWithCondition(numbers, i -> i>5)
    
  4. 懶惰帶來更高效率 使用 lambda 表達式的另一個優勢是惰性評估,例如,假設我們需要編寫一個方法來查找範圍為 3 到 11 的最大奇數並返回其平方。通常我們會像這樣編寫這個方法的代碼:

    private static int findSquareOfMaxOdd(List<Integer> numbers) {
    		int max = 0;
    		for (int i : numbers) {
    			if (i % 2 != 0 && i > 3 && i < 11 && i > max) {
    				max = i;
    			}
    		}
    		return max * max;
    	}
    

    上面的程式將始終按順序運行,但我們可以使用 Stream API 來實現這一點,並受益於懶惰求值。讓我們看看如何使用 Stream API 和 lambda 表達式以函數編程的方式重寫此代碼。

    public static int findSquareOfMaxOdd(List<Integer> numbers) {
    		return numbers.stream()
    				.filter(NumberTest::isOdd) 		//Predicate is functional interface and
    				.filter(NumberTest::isGreaterThan3)	// we are using lambdas to initialize it
    				.filter(NumberTest::isLessThan11)	// rather than anonymous inner classes
    				.max(Comparator.naturalOrder())
    				.map(i -> i * i)
    				.get();
    	}
    
    	public static boolean isOdd(int i) {
    		return i % 2 != 0;
    	}
    	
    	public static boolean isGreaterThan3(int i){
    		return i > 3;
    	}
    	
    	public static boolean isLessThan11(int i){
    		return i < 11;
    	}
    

    如果你對雙冒號(::)運算符感到驚訝,它是在 Java 8 中引入的,用於方法引用。Java 編譯器負責將參數映射到調用的方法。它是 lambda 表達式i -> isGreaterThan3(i)i -> NumberTest.isGreaterThan3(i)的簡寫。

Lambda 表達式示例

下面我提供了一些帶有小注釋的 Lambda 表達式的代碼片段。

() -> {}                     // No parameters; void result

() -> 42                     // No parameters, expression body
() -> null                   // No parameters, expression body
() -> { return 42; }         // No parameters, block body with return
() -> { System.gc(); }       // No parameters, void block body

// 複雜的塊體,帶有多個返回
() -> {
  if (true) return 10;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1             // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1                 // Single inferred-type argument, same as below
x -> x+1                   // Parenthesis optional for single inferred-type case

(String s) -> s.length()   // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length()              // Single inferred-type argument
t -> { t.start(); }          // Single inferred-type argument

(int x, int y) -> x+y      // Multiple declared-type parameters
(x,y) -> x+y               // Multiple inferred-type parameters
(x, final y) -> x+y        // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y          // Illegal: can't mix inferred and declared types

方法和構造函數引用

A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:

System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new

這就是有關 Java 8 函數介面和 Lambda 表達式教程的全部內容。我強烈建議您研究使用它,因為這種語法對 Java 是新的,需要一些時間來理解。您還應該查看Java 8功能,以了解 Java 8 發布版中的所有改進和變更。

Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces