Java 一直是一種面向對象的編程語言。這意味著 Java 編程中的一切都圍繞著對象(為了簡單起見,除了一些基本類型)。我們不僅有 Java 中的函數,它們還是 Class 的一部分,我們需要使用 class/object 來調用任何函數。 函數式介面 當我們研究一些其他的編程語言時,比如C++,J ...
Java 一直是一種面向對象的編程語言。這意味著 Java 編程中的一切都圍繞著對象(為了簡單起見,除了一些基本類型)。我們不僅有 Java 中的函數,它們還是 Class 的一部分,我們需要使用 class/object 來調用任何函數。
函數式介面
當我們研究一些其他的編程語言時,比如C++
,JavaScript
,它們被稱為函數式編程語言,因為我們可以編寫函數併在需要的時候使用它們。其中一些語言支持面向對象編程和函數式編程。
面向對象有很多優勢,但是它也使得程式變得冗長。例如,假設我們必須創建Runnable
的一個實例。通常我們使用如下的匿名類:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("This is Runnable");
}
};
從上面的代碼中我們可以發現,實際使用的部分是run()
方法中的代碼。剩下的所有代碼都是因為Java程式結構化的方式。
Java 8函數式介面和Lambda表達式通過刪除大量的固定代碼,幫助我們編寫更少、更簡潔的代碼。
Java 8 函數式介面
有且只有一個抽象方法的介面稱為函數式介面。
Java 8引入了@FunctionalInterface
註解將介面標記為函數式介面。
不是使用
@FunctionalInterface
註解的介面才是函數式介面,使用它是為了檢查函數式介面的正確性,使用它是一種規範,就像@Override
用來檢查重寫父類或實現介面的方法的正確性。
例如,我們在一個介面之上使用了該註解,併在其中添加多個抽象方法,此時會引發編譯器錯誤。
java.lang.Runnable
就是使用單個抽象方法run()
的函數式介面的一個很好的例子。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Java 8函數式介面的主要好處是,我們可以使用Lambda表達式來實例化它們,並避免使用笨重的匿名類實現。
Java 8 Collections API
已經被重寫,並且引入了新的Stream API
,其中使用了大量的函數式介面。Java 8在java.util.function
包中定義了很多函數式介面,一些常用的函數式介面包括Consumer
、Supplier
、Function
和 Predicate
等等。
下麵是一些代碼片段,以便我們更好的理解函數式介面
interface Test {
boolean equals(Object obj);
}
//Test不是函數式介面,equals是Object對象的一個成員方法
interface Comparator<T> {
boolean equals(Object obj);
int compare(T o1, T o2);
}
//Comparator是函數式介面,Comparator只有一個抽象的非Object的方法
interface Test2 {
int test2();
Object clone();
}
//Test2不是函數式介面,因為Object.clone()方法不是public而是protected
interface X {
int test(String str);
}
interface Y {
int test(String str);
}
interface Z extends X, Y {
}
//Z是函數式介面,繼承了兩個相同簽名相同返回值的方法
interface X {
List test(List<String> list);
}
interface Y {
List<String> test(List list);
}
interface Z extends X, Y {
}
//Z是函數式介面,Y.test 是一個 subsignature & return-type-substitutable
//關於subsignature & return-type-substitutable參考https://www.ssymon.com/archives/subsignature-return-type-substituable
//這裡Y.test簽名是X.test簽名的subsignature,並且Y.test的返回值類型和X.test返回值類型可以相容
interface X {
int test(List<String> list);
}
interface Y {
int test(List<Integer> list);
}
interface Z extends X, Y {
}
//Z不是函數式介面,兩個抽象方法沒有一個是subsignature
//雖然方法簽名與泛型無關,但X.test和Y.test無法相容,Z的編譯就會出錯
interface X {
long test();
}
interface Y {
int test();
}
interface Z extends X, Y {
}
//編譯出錯:methods have unrelated return types
//Z不是函數式介面,兩個方法返回值不相關不相容,沒有一個是return-type-substitutable
interface A<T> {
void test(T arg);
}
interface B<T> {
void test(T arg);
}
interface C<X, Y> extends A<X>, B<Y> {
}
//編譯錯誤:both methods have same erasure, yet neither overrides the other
//C不是函數式介面,兩個方法簽名不同,擦除之後變成相同的原生類型
Lambda表達式
通過Lambda表達式,我們可以在面向Java對象的世界中可視化函數式編程。對象是Java編程語言的基礎,沒有對象就沒有函數,這就是為什麼Java語言只支持在函數式介面中使用Lambda表達式。由於函數式介面中只有一個抽象函數,因此將Lambda表達式應用於該方法時不會出現混淆。
什麼是Lambda表達式:Lambda表達式是一個匿名函數,即沒有函數名的函數。
在Java中:所有函數都是類的成員,被稱為方法。要創建方法,您需要定義其所屬的類。
Lambda表達式語法:箭頭前面部分是方法的參數列表,後一部分方法主體
(parameters) -> expression
或
(parameters) -> { statements; }
Lambda表達式使得我們可以使用非常簡潔的語法定義一個類和單個方法,以實現具有單個抽象方法的介面,即函數式介面。
下麵我們用一些代碼來弄明白Lambda表達式如何簡化和縮短代碼,並使得代碼更具備可讀性和可維護性。
實現介面:在Java 8之前,如果要創建線程,首先要定義一個實現可運行介面的類。即函數式介面java.lang.Runnable
,其抽象方法run()
不接受任何參數。我們需要定義一個實現類來實現它。
public class TestRunnable implements Runnable{
@Override
public void run() {
System.out.println("TestRunnable is running");
}
public static void main(String[] args) {
TestRunnable r = new TestRunnable();
Thread thread = new Thread(r);
thread.start();
}
}
在此示例中,我們實現了run()
方法將一串字元串列印在控制台。然後創建一個名為 r 的實例對象,並將其傳遞給線程類的構造函數來創建一個線程對象,並調用線程的start方法。
匿名內部類:我們對上面的代碼進行一些改進,使用匿名內部類的方式來實現。
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Runnable inner class is running");
}
});
thread1.start();
}
相比實現介面的方式,匿名內部類的方式更加簡潔,無序額外新增實現類。
使用Lambda表達式:在Java 8中,使用Lambda重構的代碼更簡潔,更具可讀性。
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Runnable lambda is running"));
thread.start();
}
從上方的Lambda表達式中我們可以發現:
Runnable
是一個函數式介面,所以我們可以使用Lambda表達式來創建它的實例。run()
方法沒有參數,所以Lambda表達式也沒有參數。- 因為方法體中只有一條語句,所以可以不使用大括弧({})。對於多個語句,則必須像其他方法一樣使用大括弧,就像
if-else
塊一樣。
為什麼需要Lambda表達式
-
代碼行數減少,通過對以上的代碼對比我們可以發現使用Lambda表達式可以減少需要編寫的代碼量以及減少必須創建和維護的自定義類的數量。
比如要實現只使用一次的介面,那麼創建另一個代碼文件或另一個命名類並不總是很有意義。Lambda表達式可以定義一次匿名實現,以供一次性使用,並顯著簡化代碼。
-
行為參數化,通過行為參數化來傳遞代碼。
舉個例子,假如我們對列表中符合給定條件的數字進行求和,使用謂詞方式如下:
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) { return numbers.parallelStream() .filter(predicate) .mapToInt(i -> i) .sum(); }
使用實例:
List<Integer> numbers = new ArrayList<>(); for (int i = 0; i < 20; i++) { numbers.add(i); } //對所有數字求和 sumWithCondition(numbers, n -> true); //對偶數數字求和 sumWithCondition(numbers, i -> i % 2 == 0); //對大於5的數字求和 sumWithCondition(numbers, i -> i > 5);
再舉個例子,求出列表中3到11範圍內的最大奇數並返回它的平方:
public static int findSquareOfMaxOdd(List<Integer> numbers) { return numbers.stream() .filter(FunctionalInterfaceTest::isOdd) .filter(FunctionalInterfaceTest::isGreaterThan3) .filter(FunctionalInterfaceTest::isLessThan11) .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的方法引用(即把這個方法作為值),如上面的FunctionalInterfaceTest::isOdd
,意思是將isOdd()
方法傳遞給filter()
方法。它是Lambda表達式i -> isOdd(i)
的簡寫形式。什麼是謂詞(Predicate)?
謂詞指的是條件表達式的求值返回true或false的過程,它接受一個參數值並返回true或false。 -
在Stream API中使用,後續再詳細介紹Stream API
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
// Complex block body with multiple returns
() -> {
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
方法引用和構造方法引用示例
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
