一文讀懂函數(shù)式接口、Lambda表達(dá)式、Stream
前言
? Java 8 中引入很多有意思的新特性,本篇文章我們來聊聊其中三個比較重要的特性:函數(shù)式接口、Lambda表達(dá)式、Stream流,我們分別從示例用法、底層原理、最佳實踐三個方面來了解這些特性。
版本
? JDK 8
函數(shù)式接口
定義
? 函數(shù)式接口是 Java 8 引入的一種接口,它只包含一個抽象方法。函數(shù)式接口的存在是為了支持 Lambda 表達(dá)式,使得我們可以使用更簡潔、更靈活的方式編寫匿名函數(shù)。
@FunctionalInterface
interface Calculator {
int add(int a, int b);
default int subtract(int a, int b) {
return a - b;
}
static int multiply(int a, int b) {
return a * b;
}
}
? @FunctionalInterface 注解是可選的,推薦使用。該注解會讓編譯器強制檢查接口是否滿足函數(shù)式接口定義。
特點
? 只能有一個抽象方法,可以有參數(shù)和返回值。
? 可以包含多個默認(rèn)方法(使用 default 關(guān)鍵字)和靜態(tài)方法(使用 static 關(guān)鍵字),不違反函數(shù)式接口的定義。
說明:
默認(rèn)方法和靜態(tài)方法在 Java 8 中引入,目的是在引入新功能的同時不改變已有實現(xiàn)。
從而實現(xiàn)接口的的逐步演進(jìn),不需要同時修改所有實現(xiàn)類。
使用
@FunctionalInterface
interface Calculator {
int add(int a, int b);
default int subtract(int a, int b) {
return a - b;
}
static int multiply(int a, int b) {
return a * b;
}
}
public class TestMain {
public static void main(String[] args) {
Calculator addCalculator = (a, b) -> a + b;
System.out.println(addCalculator.add(1, 2));
System.out.println(addCalculator.subtract(1, 2));
}
}
Lambda表達(dá)式
? Lambda 表達(dá)式是一種用于傳遞匿名函數(shù)的簡潔語法。它提供了一種更緊湊的方式來表示可以傳遞給方法的代碼塊。Lambda 表達(dá)式主要用于函數(shù)式接口,可以看作是對函數(shù)式接口的一個實現(xiàn)。
Calculator addCalculator = (a, b) -> a + b;
主要場景
? 簡化匿名內(nèi)部類的寫法,但無法簡化所有匿名內(nèi)部類,只能簡化滿足函數(shù)式接口的匿名內(nèi)部類。
用法
無參寫法
? 實現(xiàn)創(chuàng)建一個簡單的線程。
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
// JDK7 匿名內(nèi)部類寫法
new Thread(new Runnable() {// 接口名
@Override
public void run() {// 方法名
System.out.println("Thread run()");
}
}).start();
// JDK8 Lambda表達(dá)式代碼塊寫法
new Thread(
() -> System.out.print("Thread run()")
).start();
有參寫法
? 實現(xiàn)根據(jù)列表中字符串元素長度進(jìn)行排序。
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
// JDK7 匿名內(nèi)部類寫法
List<String> list = Arrays.asList("my", "name", "is", "lorin");
list.sort(new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
if (s1 == null)
return -1;
if (s2 == null)
return 1;
return s1.length() - s2.length();
}
});
// JDK8 Lambda表達(dá)式寫法
List<String> list = Arrays.asList("my", "name", "is", "lorin");
list.sort((s1, s2) -> {// 省略參數(shù)表的類型
if (s1 == null)
return -1;
if (s2 == null)
return 1;
return s1.length() - s2.length();
});
Lambda 表達(dá)式的基礎(chǔ):函數(shù)式接口 + 類型推斷
? Lambda 表達(dá)式除了上文中提到的函數(shù)式接口,還有一個比較重要的特性來支持 Lambda 表達(dá)式簡潔的寫法,即類型推斷:指編譯器根據(jù)上下文信息推斷變量的類型,而不需要顯式地指定類型。類型推斷的引入是為了簡化代碼,并提高代碼的可讀性和可維護(hù)性。
CustomerInterface<Integer> action = (Integer t) -> {
System.out.println(this);
return t + 1;
};
// 使用類型推斷
CustomerInterface<Integer> action1 = t -> {
System.out.println(this);
return t + 1;
};
自定義函數(shù)接口使用 Lambda 表達(dá)式
? 首先定義一個函數(shù)接口,函數(shù)作用是對傳入的元素進(jìn)行操作,最后返回操作后的元素。
// 自定義函數(shù)接口
@FunctionalInterface
public interface CustomerInterface<T> {
T operate(T t);
}
? 自定義的 MyStream 類來使用自定義的函數(shù)接口。
class MyStream<T> {
private final List<T> list;
MyStream(List<T> list) {
this.list = list;
}
public void customerForEach(CustomerInterface<T> action) {
Objects.requireNonNull(action);
list.replaceAll(action::operate);
}
}
? 使用自定義的 MyStream 類實現(xiàn)對每一個元素的 +1 操作。
public class TestMain {
public static void main(String[] args) {
List<Integer> arr = Arrays.asList(1, 2, 3, 4);
MyStream<Integer> myStream = new MyStream<>(arr);
myStream.customerForEach(t -> t + 1);
System.out.println(arr);
}
}
// 輸出結(jié)果
[2, 3, 4, 5]
底層實現(xiàn)
? 上面我們回顧了 JDK7 和 JDK8 對匿名內(nèi)部類的寫法,我們發(fā)現(xiàn) JDK8 中的實現(xiàn)更加簡潔了,但實際上不僅僅語法上更加簡潔,即不是純粹的語法糖,底層實現(xiàn)也發(fā)生了一些變化,下面我們一起來看一下。
JDK7
- ? 由于 JDK7 并不支持函數(shù)式接口、Lambda表達(dá)式,所以我們先對代碼做一些簡單的改造:
public interface CustomerInterface<T> {
T operate(T t);
}
class MyStream<T> {
private final List<T> list;
MyStream(List<T> list) {
this.list = list;
}
public void customerForEach(CustomerInterface<T> action) {
Objects.requireNonNull(action);
for (int i = 0; i < list.size(); i++) {
list.set(i, action.operate(list.get(i)));
}
}
}
public class TestMain {
public static void main(String[] args) {
List<Integer> arr = Arrays.asList(1, 2, 3, 4);
MyStream<Integer> myStream = new MyStream<>(arr);
myStream.customerForEach(new CustomerInterface<Integer>() {
@Override
public Integer operate(Integer integer) {
return integer + 1;
}
});
System.out.println(arr);
}
}
- ? 使用 javap 分析字節(jié)碼:
javap -c -p .\TestMain.class
Compiled from "TestMain.java"
public class test.TestMain {
public test.TestMain();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_4
1: anewarray #2 // class java/lang/Integer
4: dup
5: iconst_0
6: iconst_1
7: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: aastore
11: dup
12: iconst_1
13: iconst_2
14: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: aastore
18: dup
19: iconst_2
20: iconst_3
21: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
24: aastore
25: dup
26: iconst_3
27: iconst_4
28: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
31: aastore
32: invokestatic #4 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
35: astore_1
36: new #5 // class test/MyStream
39: dup
40: aload_1
41: invokespecial #6 // Method test/MyStream."<init>":(Ljava/util/List;)V
44: astore_2
45: aload_2
46: new #7 // class test/TestMain$1 創(chuàng)建匿名內(nèi)部類
49: dup
50: invokespecial #8 // Method test/TestMain$1."<init>":()V
53: invokevirtual #9 // Method test/MyStream.customerForEach:(Ltest/CustomerInterface;)V
56: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
59: aload_1
60: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
63: return
}
? 從上面 46 行我們可以看出,JDK7 創(chuàng)建了真實的的匿名內(nèi)部類。
JDK8
? JDK8 我們以上述 自定義函數(shù)接口使用 Lambda 表達(dá)式 為例:
? 使用 javap 分析字節(jié)碼可以發(fā)現(xiàn),Lambda 表達(dá)式 被封裝為一個內(nèi)部的私有方法并通過 InvokeDynamic 調(diào)用,而不是像 JDK7 那樣創(chuàng)建一個真實的匿名內(nèi)部類。
javap -c -p .\TestMain.class
Compiled from "TestMain.java"
public class test.TestMain {
public test.TestMain();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_4
1: anewarray #2 // class java/lang/Integer
4: dup
5: iconst_0
6: iconst_1
7: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: aastore
11: dup
12: iconst_1
13: iconst_2
14: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: aastore
18: dup
19: iconst_2
20: iconst_3
21: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
24: aastore
25: dup
40: aload_1
41: invokespecial #6 // Method test/MyStream."<init>":(Ljava/util/List;)V
44: astore_2
45: aload_2
46: invokedynamic #7, 0 // InvokeDynamic #0:operate:()Ltest/CustomerInterface; InvokeDynamic 調(diào)用
51: invokevirtual #8 // Method test/MyStream.customerForEach:(Ltest/CustomerInterface;)V
54: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
57: aload_1
58: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
61: return
private static java.lang.Integer lambda$main$0(java.lang.Integer); // lambda 表達(dá)式被封裝為內(nèi)部方法
Code:
0: aload_0
1: invokevirtual #11 // Method java/lang/Integer.intValue:()I
4: iconst_1
5: iadd
6: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: areturn
}
this 的含義
? 從上面我們可以知道 JDK7 和 JDK8 對匿名內(nèi)部類不僅寫法上不一致,底層原理也不相同。因此,如果我們在兩種寫法種使用 this 關(guān)鍵字,兩者是一樣的?先說答案:不一樣,JDK7 的 this 指向創(chuàng)建的匿名內(nèi)部內(nèi),而 JDK8 中Lambda表達(dá)式并不會創(chuàng)建真實存在的類,指向的是當(dāng)前類。
? 下面我們結(jié)合實際案例來看一下:
JDK7
CustomerInterface<Integer> action = new CustomerInterface<Integer>() {
@Override
public Integer operate(Integer integer) {
System.out.println(this);
return integer + 1;
}
};
CustomerInterface<Integer> action1 = new CustomerInterface<Integer>() {
@Override
public Integer operate(Integer integer) {
System.out.println(this);
return integer + 1;
}
};
System.out.println(action.operate(2));
System.out.println(action1.operate(2));
// 輸出
test.TestMain$1@8939ec3
3
test.TestMain$2@456bf9ce
3
? 可以看到兩個 this 輸出地址不同,分別指向自身的匿名內(nèi)部類對象。
JDK8
public class TestMain {
CustomerInterface<Integer> action = t -> {
System.out.println(this);
return t + 1;
};
CustomerInterface<Integer> action1 = t -> {
System.out.println(this);
return t + 1;
};
public static void main(String[] args) {
TestMain testMain = new TestMain();
System.out.println(testMain.action.operate(2));
System.out.println(testMain.action1.operate(2));
}
}
// 輸出
test.TestMain@1d81eb93
3
test.TestMain@1d81eb93
3
? 可以看到,兩個 this 都指向同一個 testMain 對象,因為我們從前文我們可以知道 JDK8 中 Lambda 表達(dá)式 被封裝為一個內(nèi)部的私有方法并通過 InvokeDynamic 調(diào)用,而不是創(chuàng)建一個真實的匿名內(nèi)部類。
Stream
? Stream 是一種用于處理集合數(shù)據(jù)的高級抽象,它允許我們以聲明式的方式對集合進(jìn)行操作。
? 函數(shù)式接口提供了Lambda表達(dá)式的類型,Lambda表達(dá)式提供了一種簡潔的語法來定義匿名內(nèi)部類,而 Stream 提供了一種聲明式的方式來處理集合數(shù)據(jù),并與Lambda表達(dá)式無縫結(jié)合,共同支持函數(shù)式編程在Java中的應(yīng)用。
特點
? Stream 不存儲數(shù)據(jù),按照特定的規(guī)則進(jìn)行計算,最后返回計算結(jié)果。
? Stream 不改變源數(shù)據(jù)源,而返回一個新的數(shù)據(jù)源。
? Stream 是惰性計算,只有調(diào)用終端操作時,中間操作才會執(zhí)行。
操作
圖片
Stream 流創(chuàng)建
? Stream 流支持并行流和串行流兩種方式,串行流每個元素按照順序依次處理,并行流會將流中元素拆分為多個子任務(wù)進(jìn)行處理,最后再合并結(jié)果,從而提高處理效率。
List<String> list = Arrays.asList("11", "2222", "333333");
// 串行流
list.stream().map(String::toString).collect(Collectors.toList());
// 并行流
list.parallelStream().map(String::toString).collect(Collectors.toList());
list.stream().parallel().map(String::toString).collect(Collectors.toList());
中間操作和終端操
中間操作
? 只會記錄操作不會立即執(zhí)行,中間操作可以細(xì)分為:無狀態(tài) Stateless 和 有狀態(tài) Stateful 兩種。
無狀態(tài) Stateless
? 指元素不受其它元素影響,可以繼續(xù)往下執(zhí)行,比如 filter() map() mapToInt() 等。
filter
? 用于篩選符合條件的元素,下一步只會拿到符合條件的元素。
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 獲取空字符串的數(shù)量
long count = strings.stream().filter(string -> string.isEmpty()).count();
map
? 用于將一個流中的元素通過指定的映射函數(shù)轉(zhuǎn)換為另一個流。返回類型必須是傳入類型或傳入類型的子類型。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 map 方法將列表中的每個元素乘以2
List<Integer> doubledNumbers = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
mapToInt() mapToLong() 等
? mapToInt() 方法用于將流中的元素映射為 int 類型的流。IntStream 是針對 int 類型數(shù)據(jù)進(jìn)行優(yōu)化的特殊流,提供了更高效的操作和更方便的處理方式。當(dāng)處理基本類型 int 數(shù)據(jù)時,推薦使用 IntStream,可以提高代碼的性能和可讀性。
? mapToLong() 方法用于將流中的元素映射為 long 類型的流。
// 整數(shù)列表
Long[] numbers = {1, 2, 3, 4, 5};
// 使用 mapToLong() 方法將每個整數(shù)乘以自身,并收集到一個 LongStream 流中
LongStream squares = Arrays.stream(numbers).mapToLong(t -> t * t);
squares.sum();
flatMap() flatMapToInt() 等
? flatMap()用于將流中的每個元素映射為一個流,然后將所有映射得到的流合并成一個新的流。
? flatMapToInt() 和 flatMap() 的區(qū)別在于返回的流為 IntStream。
// 字符串列表
List<String> words = Arrays.asList("Java is fun", "Stream API is powerful", "FlatMap is useful");
// 使用 flatMap() 提取每個字符串中的單詞,并放入一個新的流中
Stream<String> wordStream = words.stream()
.flatMap(str -> Arrays.stream(str.split("\\s+")));
// 打印流中的每個單詞
wordStream.forEach(System.out::println);
// 輸出
Java
is
fun
Stream
API
is
powerful
FlatMap
is
useful
peek
? 用于在流的每個元素上執(zhí)行指定的操作,同時保留流中的元素。peek() 方法不會改變流中的元素,而是提供一種查看每個元素的機(jī)會,通常用于調(diào)試、日志記錄或記錄流中的中間狀態(tài)。
// 整數(shù)列表
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 peek() 打印每個元素,并將元素乘以2,然后收集到一個新的列表中
List<Integer> doubledNumbers = numbers.stream()
.peek(num -> System.out.println("Original: " + num))
.map(num -> num * 2)
.peek(doubledNum -> System.out.println("Doubled: " + doubledNum))
.collect(Collectors.toList());
// 打印新列表中的元素
System.out.println("Doubled Numbers: " + doubledNumbers);
有狀態(tài) Stateful
? 指元素受到其它元素影響,比如 distinct() 去重,需要處理完所有元素才能往下執(zhí)行。
distinct
? 用于去除流中重復(fù)的元素,返回一個去重后的新流。distinct() 方法根據(jù)元素的 equals() 方法來判斷是否重復(fù),因此流中的元素必須實現(xiàn)了 equals() 方法以確保正確的去重。
// 字符串列表
List<String> words = Arrays.asList("hello", "world", "hello", "java", "world");
// 使用 distinct() 方法獲取不重復(fù)的單詞,并收集到一個新的列表中
List<String> uniqueWords = words.stream()
.distinct()
.collect(Collectors.toList());
// 打印不重復(fù)的單詞列表
System.out.println("Unique Words: " + uniqueWords);
limit
? 用于限制流中元素的數(shù)量,返回一個包含了指定數(shù)量元素的新流。limit() 方法通常用于在處理大型數(shù)據(jù)集時,限制處理的數(shù)據(jù)量,以提高性能或減少資源消耗。需要注意的,返回的元素不一定是前三個。
// 整數(shù)列表
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 limit() 方法獲取前3個元素,并收集到一個新的列表中
List<Integer> limitedNumbers = numbers.stream()
.limit(3)
.collect(Collectors.toList());
// 打印前3個元素
System.out.println("Limited Numbers: " + limitedNumbers);
終端操作
? 調(diào)用終端操作計算會立即開始執(zhí)行,終端操作可以細(xì)分為:非短路操作 和 短路操作。
非短路操作
? 非短路操作:需要處理完所有元素才可以拿到結(jié)果,比如 forEach() forEachOrdered()。
collect
? 將流中的元素收集到一個集合或者其他數(shù)據(jù)結(jié)構(gòu)中。下面是一些常見的用法:
// 將流中的元素收集到一個列表中:
List<String> list = stream.collect(Collectors.toList());
// 將流中的元素收集到一個集合中:
Set<String> set = stream.collect(Collectors.toSet());
// 將流中的元素收集到一個指定類型的集合中:
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
// 將流中的元素收集到一個字符串中,使用指定的分隔符連接:
String result = stream.collect(Collectors.joining(", "));
// 將流中的元素收集到一個 Map 中,根據(jù)指定的鍵值對:
Map<Integer, String> map = stream.collect(Collectors.toMap(String::length, Function.identity()));
// 對流中的元素進(jìn)行分組:
Map<Integer, List<String>> groupedMap = stream.collect(Collectors.groupingBy(String::length));
// 對流中的元素進(jìn)行分區(qū):
Map<Boolean, List<String>> partitionedMap = stream.collect(Collectors.partitioningBy(s -> s.length() > 3));
// 對流中的元素進(jìn)行統(tǒng)計:
IntSummaryStatistics statistics = stream.collect(Collectors.summarizingInt(String::length));
reduce
- ? 用于將流中的元素組合成一個值。
- ? 靈活性:reduce() 方法提供了靈活的參數(shù)選項,可以根據(jù)需求選擇不同的重載形式,包括指定初始值、選擇累加器函數(shù)和組合器函數(shù)等,使得它可以適用于各種場景。
- ? 統(tǒng)一操作:reduce() 方法提供了一種統(tǒng)一的方式來對流中的元素進(jìn)行組合操作,不論是求和、求積、字符串拼接還是其他任何類型的組合操作,都可以使用 reduce() 方法來實現(xiàn),這樣可以減少代碼重復(fù),提高代碼的可讀性和可維護(hù)性。
- ? 并行流支持:在并行流中,reduce() 方法可以更高效地利用多核處理器,通過并行化操作來提高性能。使用合適的組合器函數(shù),可以在并行流中正確地合并部分結(jié)果,從而實現(xiàn)更高效的并行計算。而 sum() 函數(shù)是串行的。
// 將流中的元素累加求和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
System.out.println("Sum: " + sum.orElse(0)); // 輸出 15
// 使用初始值進(jìn)行累加求和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum); // 輸出 15
// 使用初始值和組合器函數(shù)在并行流中進(jìn)行累加求和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream().reduce(0, (a, b) -> a + b, Integer::sum);
System.out.println("Sum: " + sum); // 輸出 15
短路操作
? 短路操作:得到符合條件的元素就可以立即返回,而不用處理所有元素,比如 anyMatch() allMatch()。
findFirst
? 用于獲取流中的第一個元素(如果存在的話),返回一個 Optional 對象。注意:返回值不一定為第一個元素。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstNumber = numbers.stream().findFirst();
if (firstNumber.isPresent()) {
System.out.println("First number: " + firstNumber.get()); // 輸出 First number: 1
} else {
System.out.println("No elements found in the stream.");
}
總結(jié)
? 函數(shù)式接口、Lambda表達(dá)式和Stream是Java 8引入的重要特性,它們使得Java代碼更加簡潔、靈活、易讀。函數(shù)式接口定義了一種新的編程模式,Lambda表達(dá)式提供了一種更加簡潔的語法來實現(xiàn)函數(shù)式接口,Stream則提供了一套豐富的操作方法來處理集合數(shù)據(jù)。通過這些特性的組合應(yīng)用,可以極大地提高Java代碼的開發(fā)效率和質(zhì)量。
? 本文篇幅有限,Stream 部分僅介紹了基本定義和常見的用法,沒有對 Stream 底層原理(并行、串行等)做深入解析,這部分將在下一篇文章中介紹。