歡迎來到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功能接口包括Consumer
、Supplier
、Function
和Predicate
。您可以在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 表達式
-
代碼行數減少使用 Lambda 表達式的一個明顯好處是代碼量減少,我們已經看到了如何使用 Lambda 表達式輕鬆創建函數介面的實例,而不是使用匿名類別。
-
順序和並行執行支援使用 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() 方法返回此流的元素是否均不匹配提供的斷言。如果不需要確定結果,則可能不會對所有元素進行斷言評估。
-
將行為傳遞到方法 讓我們看看如何使用 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)
-
懶惰帶來更高效率 使用 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