Java Stream的延遲加載與短路操作詳解
一、引言
在 Java 8 引入 Stream API 后,開發(fā)者處理集合數(shù)據(jù)的方式發(fā)生了革命性的變化。Stream API 提供了一種簡潔、高效的流式數(shù)據(jù)處理模式,允許開發(fā)者以聲明式的方式對數(shù)據(jù)進(jìn)行過濾、映射、歸約等操作。在 Stream 的眾多特性中,** 延遲加載(Lazy Evaluation)和短路操作(Short-Circuiting Operations)** 是實(shí)現(xiàn)高效數(shù)據(jù)處理的關(guān)鍵,它們能夠顯著減少不必要的計(jì)算,提升程序性能,尤其在處理大規(guī)模數(shù)據(jù)集時(shí)效果更為明顯。
二、Stream 基礎(chǔ)概念回顧
在深入探討延遲加載與短路操作之前,有必要先回顧一下 Stream 的基本概念。Stream 是 Java 8 中對集合數(shù)據(jù)處理的一種抽象,它代表了一系列支持連續(xù)、批量操作的數(shù)據(jù)元素。Stream 本身并不存儲數(shù)據(jù),而是通過對數(shù)據(jù)源(如集合、數(shù)組)進(jìn)行操作,生成一個(gè)新的 Stream,每個(gè) Stream 操作可以分為中間操作(Intermediate Operations)和終端操作(Terminal Operations)。
- 中間操作:例如
filter、map、limit等,它們會返回一個(gè)新的 Stream,并且不會立即執(zhí)行,而是等到終端操作觸發(fā)時(shí)才執(zhí)行,這是實(shí)現(xiàn)延遲加載的基礎(chǔ)。中間操作主要用于對 Stream 中的元素進(jìn)行轉(zhuǎn)換、過濾等處理,為后續(xù)的計(jì)算做準(zhǔn)備。 - 終端操作:例如
forEach、collect、count、anyMatch等,當(dāng)終端操作被調(diào)用時(shí),整個(gè) Stream 操作鏈才會被執(zhí)行,并且會產(chǎn)生最終的結(jié)果。終端操作會觸發(fā)中間操作的執(zhí)行,并將結(jié)果返回給調(diào)用者。
三、延遲加載(Lazy Evaluation)
3.1 延遲加載的定義與原理
延遲加載是指 Stream 的中間操作不會立即執(zhí)行,而是將操作記錄下來,形成一個(gè)操作鏈。直到終端操作被調(diào)用時(shí),才會一次性地從數(shù)據(jù)源開始,按照操作鏈的順序執(zhí)行所有的中間操作和終端操作。這種機(jī)制避免了在數(shù)據(jù)處理過程中不必要的計(jì)算,只有當(dāng)真正需要結(jié)果時(shí)才進(jìn)行計(jì)算,大大提高了數(shù)據(jù)處理的效率。
以一個(gè)簡單的示例來說明延遲加載的原理:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Stream<Integer> stream = numbers.stream()
.filter(n -> {
System.out.println("Filtering: " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("Mapping: " + n);
return n * n;
});在上述代碼中,我們創(chuàng)建了一個(gè) Stream,并對其進(jìn)行了filter和map兩個(gè)中間操作。但是,當(dāng)執(zhí)行到這一步時(shí),控制臺并不會輸出任何信息,因?yàn)檫@兩個(gè)中間操作并沒有立即執(zhí)行,它們只是被記錄在操作鏈中。
只有當(dāng)我們添加一個(gè)終端操作,例如forEach時(shí),整個(gè)操作鏈才會被執(zhí)行:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers.stream()
.filter(n -> {
System.out.println("Filtering: " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("Mapping: " + n);
return n * n;
})
.forEach(System.out::println);此時(shí),控制臺會按照操作鏈的順序輸出過濾和映射過程中的信息,并最終輸出處理后的結(jié)果。這就是延遲加載的核心原理,它將多個(gè)操作組合在一起,在需要結(jié)果時(shí)才一次性執(zhí)行,減少了中間過程的開銷。
3.2 延遲加載的優(yōu)勢
- 減少不必要的計(jì)算:在處理大規(guī)模數(shù)據(jù)集時(shí),延遲加載可以避免對所有數(shù)據(jù)進(jìn)行不必要的中間操作。例如,當(dāng)我們只需要獲取 Stream 中的前幾個(gè)元素時(shí)(使用limit操作),如果沒有延遲加載,所有的中間操作都會作用于整個(gè)數(shù)據(jù)集,而有了延遲加載,一旦滿足limit的條件,后續(xù)的中間操作就不會再執(zhí)行,從而節(jié)省了大量的計(jì)算資源。
List<Integer> largeList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
largeList.add(i);
}
largeList.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.limit(10)
.forEach(System.out::println);在上述代碼中,由于使用了limit(10),當(dāng)獲取到前 10 個(gè)滿足條件的元素后,filter和map操作就不會再對剩余的元素進(jìn)行處理,大大提高了效率。
- 提高代碼的可讀性和靈活性:延遲加載使得開發(fā)者可以將多個(gè)數(shù)據(jù)處理操作鏈?zhǔn)降亟M合在一起,代碼更加簡潔明了,易于理解和維護(hù)。同時(shí),通過調(diào)整操作鏈中的操作順序和類型,可以靈活地實(shí)現(xiàn)不同的數(shù)據(jù)處理邏輯。
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
words.stream()
.map(String::toUpperCase)
.filter(s -> s.length() > 5)
.sorted()
.forEach(System.out::println);在這個(gè)示例中,我們通過鏈?zhǔn)秸{(diào)用map、filter和sorted操作,清晰地表達(dá)了對字符串列表的處理邏輯,即先將所有字符串轉(zhuǎn)換為大寫,然后過濾出長度大于 5 的字符串,最后進(jìn)行排序并輸出。
3.3 延遲加載的應(yīng)用場景
- 數(shù)據(jù)過濾與轉(zhuǎn)換:在從數(shù)據(jù)庫或文件中讀取大量數(shù)據(jù)并進(jìn)行處理時(shí),延遲加載可以先將數(shù)據(jù)以 Stream 的形式讀取進(jìn)來,然后通過中間操作進(jìn)行過濾和轉(zhuǎn)換,最后再通過終端操作獲取所需的結(jié)果。這樣可以避免一次性將所有數(shù)據(jù)加載到內(nèi)存中進(jìn)行處理,降低內(nèi)存壓力。
List<Product> products = productRepository.findAll();
products.stream()
.filter(Product::isInStock)
.map(Product::getPrice)
.map(price -> price * 0.9) // 打9折
.collect(Collectors.toList());在上述代碼中,我們從數(shù)據(jù)庫中獲取產(chǎn)品列表后,通過延遲加載的方式對產(chǎn)品進(jìn)行過濾和價(jià)格計(jì)算,最后將處理后的價(jià)格收集到一個(gè)列表中。
- 流式計(jì)算與聚合:在進(jìn)行復(fù)雜的聚合計(jì)算時(shí),延遲加載可以將多個(gè)中間操作組合起來,對數(shù)據(jù)進(jìn)行逐步處理,最后再進(jìn)行聚合。例如,計(jì)算一組數(shù)據(jù)的平均值、總和等。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
double average = numbers.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0);在這個(gè)示例中,我們先將List<Integer>轉(zhuǎn)換為IntStream,然后通過average終端操作計(jì)算平均值。在這個(gè)過程中,mapToInt中間操作是延遲執(zhí)行的,直到調(diào)用average時(shí)才會真正執(zhí)行,從而實(shí)現(xiàn)了高效的計(jì)算。
四、短路操作(Short-Circuiting Operations)
4.1 短路操作的定義與原理
短路操作是 Stream API 中的一種特殊機(jī)制,它指的是在某些情況下,當(dāng) Stream 操作滿足一定條件時(shí),后續(xù)的操作會被立即終止,不再繼續(xù)執(zhí)行。短路操作主要應(yīng)用于中間操作(如limit、takeWhile)和終端操作(如anyMatch、allMatch、noneMatch)中。
以anyMatch終端操作為例,它的作用是判斷 Stream 中是否存在至少一個(gè)元素滿足給定的條件。當(dāng) Stream 中的某個(gè)元素滿足條件時(shí),anyMatch操作會立即返回true,并且不會再對后續(xù)的元素進(jìn)行判斷。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean hasEven = numbers.stream()
.anyMatch(n -> {
System.out.println("Checking: " + n);
return n % 2 == 0;
});
System.out.println("Has even number: " + hasEven);在上述代碼中,當(dāng) Stream 遍歷到第一個(gè)偶數(shù)2時(shí),anyMatch操作就會返回true,控制臺只會輸出Checking: 1和Checking: 2,后續(xù)元素的判斷操作會被短路,不再執(zhí)行。
4.2 常見的短路操作
終端短路操作:
- anyMatch:判斷 Stream 中是否存在至少一個(gè)元素滿足給定的條件。一旦找到滿足條件的元素,就會立即返回true,不再繼續(xù)遍歷。
- allMatch:判斷 Stream 中的所有元素是否都滿足給定的條件。只要有一個(gè)元素不滿足條件,就會立即返回false,停止遍歷。
- noneMatch:判斷 Stream 中是否沒有任何元素滿足給定的條件。一旦找到一個(gè)滿足條件的元素,就會立即返回false,不再繼續(xù)遍歷。
List<Integer> numbers = Arrays.asList(1, 3, 5, 7);
boolean allOdd = numbers.stream()
.allMatch(n -> n % 2 != 0);
boolean noneEven = numbers.stream()
.noneMatch(n -> n % 2 == 0);在上述代碼中,allMatch操作在遍歷到第一個(gè)元素1時(shí),會繼續(xù)檢查后續(xù)元素,直到確認(rèn)所有元素都為奇數(shù)才返回true;而noneMatch操作只要遇到一個(gè)偶數(shù)元素就會返回false,如果遍歷完所有元素都沒有偶數(shù)元素,則返回true。
中間短路操作:
- limit:截取 Stream 中的前n個(gè)元素,生成一個(gè)新的 Stream。當(dāng)截取到足夠數(shù)量的元素后,后續(xù)的元素就不會再被處理。
- takeWhile:從 Stream 的開頭開始,提取滿足給定條件的元素,直到遇到不滿足條件的元素為止。一旦遇到不滿足條件的元素,就會停止提取。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> limited = numbers.stream()
.limit(3)
.collect(Collectors.toList());
List<Integer> taken = numbers.stream()
.takeWhile(n -> n < 4)
.collect(Collectors.toList());在上述代碼中,limit(3)操作會截取 Stream 中的前 3 個(gè)元素,即使后續(xù)還有元素,也不會再進(jìn)行處理;takeWhile(n -> n < 4)操作會從 Stream 開頭提取小于 4 的元素,當(dāng)遇到元素4時(shí),就會停止提取。
4.3 短路操作的優(yōu)勢與應(yīng)用場景
- 提高性能:在處理大規(guī)模數(shù)據(jù)集時(shí),短路操作可以顯著減少不必要的計(jì)算,提高程序的執(zhí)行效率。例如,在使用anyMatch判斷集合中是否存在滿足特定條件的元素時(shí),如果數(shù)據(jù)集很大,一旦找到滿足條件的元素,就可以立即返回結(jié)果,避免遍歷整個(gè)數(shù)據(jù)集。
- 簡化邏輯判斷:短路操作可以使代碼更加簡潔,通過使用allMatch、noneMatch等操作,可以清晰地表達(dá)對數(shù)據(jù)的邏輯判斷需求,避免編寫復(fù)雜的循環(huán)和條件判斷語句。
List<Employee> employees = employeeService.getEmployees();
boolean allFullTime = employees.stream()
.allMatch(Employee::isFullTime);
boolean noOverworked = employees.stream()
.noneMatch(e -> e.getHoursWorked() > 60);在上述代碼中,通過allMatch和noneMatch操作,我們可以簡潔地判斷員工列表中是否所有員工都是全職,以及是否沒有員工加班超過 60 小時(shí),使代碼邏輯更加清晰易懂。
五、延遲加載與短路操作的結(jié)合應(yīng)用
延遲加載和短路操作通常會結(jié)合在一起發(fā)揮作用,進(jìn)一步提升 Stream 數(shù)據(jù)處理的效率。當(dāng)一個(gè) Stream 操作鏈中同時(shí)包含延遲加載的中間操作和短路操作時(shí),只有在必要的情況下,才會對數(shù)據(jù)進(jìn)行處理,最大限度地減少計(jì)算量。
例如,我們有一個(gè)需求,從一個(gè)包含大量商品的列表中,判斷是否存在價(jià)格大于 100 且?guī)齑娲笥?10 的商品:
List<Product> products = productRepository.findAll();
boolean exists = products.stream()
.filter(p -> {
System.out.println("Filtering by price: " + p.getPrice());
return p.getPrice() > 100;
})
.filter(p -> {
System.out.println("Filtering by stock: " + p.getStock());
return p.getStock() > 10;
})
.anyMatch(p -> true);
System.out.println("Exists product: " + exists);在上述代碼中,filter操作是延遲加載的中間操作,anyMatch是短路操作。當(dāng) Stream 在進(jìn)行第一個(gè)filter操作時(shí),只有當(dāng)遇到價(jià)格大于 100 的商品后,才會繼續(xù)進(jìn)行第二個(gè)filter操作。而一旦在第二個(gè)filter操作中找到庫存大于 10 的商品,anyMatch操作就會立即返回true,后續(xù)的元素就不會再被處理。這樣,通過延遲加載和短路操作的結(jié)合,我們可以高效地完成數(shù)據(jù)判斷任務(wù),避免了對大量不必要數(shù)據(jù)的處理。
六、性能分析與注意事項(xiàng)
6.1 性能分析
延遲加載和短路操作在提升 Stream 數(shù)據(jù)處理性能方面具有顯著的效果,但具體的性能提升程度會受到多種因素的影響,如數(shù)據(jù)集的大小、操作的復(fù)雜度、硬件資源等。
在處理小規(guī)模數(shù)據(jù)集時(shí),延遲加載和短路操作帶來的性能提升可能并不明顯,因?yàn)閿?shù)據(jù)處理的開銷相對較小,而操作鏈的構(gòu)建和管理也會有一定的開銷。然而,當(dāng)數(shù)據(jù)集規(guī)模增大時(shí),它們的優(yōu)勢就會逐漸顯現(xiàn)出來。通過減少不必要的計(jì)算,延遲加載和短路操作可以大大降低 CPU 和內(nèi)存的使用,提高程序的執(zhí)行速度。
例如,在一個(gè)包含 100 萬個(gè)元素的列表中,使用傳統(tǒng)的循環(huán)和條件判斷來查找滿足特定條件的元素,可能需要遍歷整個(gè)列表,花費(fèi)較長的時(shí)間。而使用 Stream 的延遲加載和短路操作,如anyMatch,一旦找到滿足條件的元素,就會立即停止遍歷,能夠在極短的時(shí)間內(nèi)得到結(jié)果,性能提升非常顯著。
6.2 注意事項(xiàng)
操作順序的影響:在構(gòu)建 Stream 操作鏈時(shí),操作的順序會影響性能和結(jié)果。通常,應(yīng)該將過濾操作盡量放在前面,這樣可以盡早減少數(shù)據(jù)量,避免后續(xù)操作處理不必要的數(shù)據(jù)。例如,在進(jìn)行map和filter操作時(shí),如果先進(jìn)行filter操作,過濾掉不滿足條件的元素后,再進(jìn)行map操作,會比先map后filter更加高效。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 推薦寫法,先過濾再映射
numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.forEach(System.out::println);
// 不推薦寫法,先映射會處理更多數(shù)據(jù)
numbers.stream()
.map(n -> n * n)
.filter(n -> n % 2 == 0)
.forEach(System.out::println);在上述代碼中,第一種寫法先過濾出偶數(shù),再對偶數(shù)進(jìn)行平方運(yùn)算,處理的數(shù)據(jù)量相對較少;而第二種寫法先對所有數(shù)字進(jìn)行平方運(yùn)算,然后再過濾,處理的數(shù)據(jù)量更大,效率更低。
- 避免過度使用:雖然延遲加載和短路操作可以提高性能,但也不要過度使用復(fù)雜的操作鏈。過于復(fù)雜的操作鏈可能會使代碼難以理解和維護(hù),并且在某些情況下,可能會因?yàn)椴僮麈湹臉?gòu)建和管理開銷過大,反而降低性能。因此,在實(shí)際應(yīng)用中,需要根據(jù)具體的需求和數(shù)據(jù)特點(diǎn),合理地選擇和組合 Stream 操作。
- 理解操作的副作用:在使用 Stream 操作時(shí),要注意某些操作可能會產(chǎn)生副作用。例如,在
forEach操作中修改外部變量,可能會導(dǎo)致不可預(yù)測的結(jié)果。因?yàn)?Stream 操作是并行執(zhí)行時(shí),多個(gè)線程同時(shí)訪問和修改外部變量會引發(fā)線程安全問題。所以,應(yīng)該盡量避免在 Stream 操作中產(chǎn)生副作用,保持操作的純粹性。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int[] sum = {0};
numbers.stream()
.forEach(n -> sum[0] += n); // 不推薦,存在副作用,并行執(zhí)行時(shí)結(jié)果不準(zhǔn)確在上述代碼中,通過在forEach操作中修改sum數(shù)組,這種方式在并行 Stream 中是不安全的,因?yàn)槎鄠€(gè)線程可能同時(shí)訪問和修改sum數(shù)組,導(dǎo)致結(jié)果不準(zhǔn)確。正確的做法是使用reduce等聚合操作來計(jì)算總和。
七、總結(jié)
Java Stream 的延遲加載和短路操作是其實(shí)現(xiàn)高效數(shù)據(jù)處理的重要特性。延遲加載通過將中間操作的執(zhí)行推遲到終端操作調(diào)用時(shí),減少了不必要的計(jì)算;短路操作則在滿足特定條件時(shí),立即終止后續(xù)操作,進(jìn)一步提高了性能。這兩個(gè)特性相互配合,在處理大規(guī)模數(shù)據(jù)集和復(fù)雜數(shù)據(jù)處理邏輯時(shí),能夠顯著提升程序的執(zhí)行效率,同時(shí)使代碼更加簡潔、易讀。
以上就是Java Stream的延遲加載與短路操作詳解的詳細(xì)內(nèi)容,更多關(guān)于Java Stream延遲加載與短路的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot中對應(yīng)2.0.x版本的Redis配置詳解
這篇文章主要為大家介紹了SpringBoot中對應(yīng)2.0.x版本的Redis配置詳解,文中的實(shí)現(xiàn)步驟講解詳細(xì),感興趣的小伙伴們可以了解一下2022-06-06
SpringBoot默認(rèn)包掃描機(jī)制及@ComponentScan指定掃描路徑詳解
這篇文章主要介紹了SpringBoot默認(rèn)包掃描機(jī)制及@ComponentScan指定掃描路徑詳解,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
java 數(shù)據(jù)結(jié)構(gòu)并查集詳解
并查集是一種用來管理元素分組情況的數(shù)據(jù)結(jié)構(gòu)。并查集可以高效地進(jìn)行如下操作。本文將通過Java實(shí)現(xiàn)并查集,感興趣的小伙伴可以了解一下2022-03-03
如何在Spring?Boot中使用MyBatis訪問數(shù)據(jù)庫
MyBatis可以通過簡單的XML或者注解來配置和映射原始類型,接口,和Java POJO為數(shù)據(jù)庫中記錄,使用MyBatis幫助我們解決各種問題,本文介紹如何在Spring?Boot中使用MyBatis訪問數(shù)據(jù)庫,感興趣的朋友一起看看吧2023-11-11
使用Spring的FactoryBean創(chuàng)建和獲取Bean對象方式
這篇文章主要介紹了使用Spring的FactoryBean創(chuàng)建和獲取Bean對象方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03
Java中利用Alibaba開源技術(shù)EasyExcel來操作Excel表的示例代碼
這篇文章主要介紹了Java中利用Alibaba開源技術(shù)EasyExcel來操作Excel表的示例代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
淺談Arrays.asList() 和ArrayList類型區(qū)別
下面小編就為大家?guī)硪黄狝rrays.asList() 和ArrayList類型區(qū)別。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-10-10

