當涉及 Java 程式設計時,Java Stream 是一個功能強大且高效的工具,用于處理集合資料。它提供了一種聲明式的方式來操作資料,可以顯著簡化代碼并提高可讀性。在本文中,我們将深入探讨 Java Stream,介紹其基本概念、常用操作和用例。
什麼是 Java Stream
Java Stream 是 Java 8 引入的一種新的抽象層,用于處理集合資料(如清單、數組等)。它允許你以一種更簡潔、更聲明式的方式對資料進行操作,而無需編寫冗長的循環和條件語句。
其核心類庫主要改進了對集合類的 API 和新增 Stream 操作。Stream類中每一個方法都對應集合上的一種操作。将真正的函數式程式設計引入到Java中,能讓代碼更加簡潔,極大地簡化了集合的處理操作,提高了開發的效率和生産力。
同時 Stream 不是一種資料結構,它隻是某種資料源的一個視圖,資料源可以是一個數組,Java容器或I/O channel 等。在 Stream 中的操作每一次都會産生新的流,内部不會像普通集合操作一樣立刻擷取值,而是惰性取值,隻有等到使用者真正需要結果的時候才會執行。
通過使用 Stream,你可以對資料執行各種操作,如篩選、映射、排序、聚合等。
集合操作與 Stream 操作對比
集合操作
處理方式
集合操作是通過疊代和循環來處理集合中的元素。你需要編寫顯式的循環代碼,手動通路和操作每個元素。
适用場景
集合操作适用于簡單的資料處理需求,或者在需要直接操作集合内部的情況下。它在處理小型資料集時可能較為友善。
優點
- 直覺:集合操作可以更直覺地展示資料的處理過程。
- 熟悉:如果你對循環和疊代操作非常熟悉,可能更容易上手。
缺點
- 備援:集合操作通常需要編寫較多的循環和條件語句,導緻代碼備援。
- 低效:在大資料集上,集合操作可能會導緻性能問題,因為它需要顯式疊代每個元素。
Stream 操作
處理方式
Stream 操作使用聲明式的方式來操作集合資料。你可以鍊式調用多個操作,以流水線的方式對資料進行處理。
适用場景
Stream 操作适用于需要對資料進行篩選、轉換、排序、聚合等複雜操作的情況。它在處理大型資料集和需要并行處理的情況下更為有效。
優點
- 簡潔:Stream 操作可以以更簡潔的方式表達資料處理邏輯,減少了循環和條件的使用。
- 高效:Stream 操作可以在内部進行優化,如并行處理,進而在大資料集上提供更高的性能。
缺點
- 學習曲線:如果你對 Stream 操作不熟悉,可能需要一些時間來學習其概念和文法。
- 不适合直接操作集合:Stream 操作通常不直接修改原始集合,而是生成新的 Stream 或最終結果。
案例
本示例以操作訂單金額集合為例,查詢金額小于 100 的訂單,并且根據建立時間進行排序,得到訂單标題,生成新集合
java複制代碼public class List {
public static void main(String[] args) {
java.util.List<Order> orders = new ArrayList<>();
orders.add( new Order(1, "水果", 18, "2023-08-13"));
orders.add( new Order(2, "衣服", 59, "2023-08-12"));
orders.add( new Order(3, "日用品", 28, "2023-08-11"));
orders.add( new Order(4, "電子産品", 8999, "2023-08-10"));
orders.add( new Order(5, "圖書", 128, "2023-08-09"));
java.util.List<Order> result = new ArrayList<>();
// 過濾出價格大于100的訂單
for (Order order : orders) {
if (order.getPrice() > 100){
result.add(order);
}
}
// 按照日期進行排序
Collections.sort(result,new Comparator<Order>(){
@Override
public int compare(Order o1, Order o2) {
return Integer.compare(o1.getDate(),o2.getDate());
}
});
// 取出訂單名稱
ArrayList<Order> names = new ArrayList<>();
for (Order order : result) {
names.add(order.getName());
}
// 列印
System.out.println(names.toString());
}
}
上述示例代碼代碼中産生了兩個多餘的聲明,result 和 names,這兩個的聲明在整個過程中的作用是作為一個中間資料存儲容器,而且還需要将整體的操作多次周遊。
那麼通過 Stream 流是如何處理上述資料的,請看下面示例
java複制代碼public class Stream {
public static void main(String[] args) {
java.util.List<Order> orders = new ArrayList<>();
orders.add( new Order(1, "水果", 18, "2023-08-13"));
orders.add( new Order(2, "衣服", 59, "2023-08-12"));
orders.add( new Order(3, "日用品", 28, "2023-08-11"));
orders.add( new Order(4, "電子産品", 8999, "2023-08-10"));
orders.add( new Order(5, "圖書", 128, "2023-08-09"));
// 直接周遊輸出
orders.stream()
.filter(order -> order.getPrice() > 100) // 過濾出價格大于100的訂單
.sorted(Comparator.comparing(Order::getDate)) // 按照日期進行排序
.map(Order::getName)// 取出訂單名稱
.forEach(System.out::println); // 周遊輸出
// 得到新的集合進行輸出
List<String> result = orders.stream()
.filter(order -> order.getPrice() > 100).sorted(Comparator.comparing(Order::getDate))
.map(Order::getName).collect(Collectors.toList());
System.out.println(result);
}
}
通過上述代碼的執行,可以發現無需再去定義過多的備援變量。我們可以将多個操作組成一個調用鍊,形成資料處理流水線。在減少代碼量的同時也更加的清晰易懂。
并且對于現在調用的方法,本身都是一種高層次構件,與線程模型無關。是以在并行使用中,開發者們無需再去操心線程和鎖了。Stream内部都已經做好了。
更好的了解 Sream 流操作
把 Stream 流操作了解成資料庫的查詢操作
集合=資料表 元素=表中的每條資料 屬性=資料列 流 API = SQL 查詢
Stream 流主要思想
Stream 流思想就是将集合的操作由傳統的 for 循環式(外部疊代)轉變為 Stream 流式操作(内部疊代)
外部疊代
所有的集合疊代所及都是在我們自己編寫的代碼中,是以這種顯式的方式稱之為外部疊代。其主要關注于資料本身。并且一般都是串行的。
1)for 循環是串行的,而且必須按照集合中元素的順序進行依次處理,要想改造成并行的話,需要修改每個for循環 2)使用是及早求值,傳回的是另一個值或空。使用性能上存在一點點的瑕疵 3)易讀性不好,如果for中嵌套大量循環與功能代碼,閱讀起來簡直是災難
内部疊代
而内部疊代來說,它所操作的就是不是一個集合了,而是一個流。它會将所有的操作融合在流中,由其在内部進行處
理,這種隐式的方式稱之為内部疊代。并且内部疊代支援并行處理,更利于集合操作的性能優化。其關注與對資料的計算。
Stream 操作詳解
Stream 流接口中定義了許多對于集合的操作方法,總的來說分為以下兩大類
- 中間操作:會傳回一個流,通過這種方式可以将多個中間操作連接配接起來,形成一個調用鍊,進而轉換為另外一個流。除非調用鍊最後存在一個終端操作,否則中間操作對流不會進行任何結果處理。
- 終端操作:會傳回一個具體的結果,比如 boolean、list、integer 等類型結果。
篩選操作
對于集合的操作,經常性的會涉及到對于集中符合條件的資料篩選,Stream 中對于資料篩選兩個常見的API:filter(過濾)、distinct(去重)
filter
filter 是 Stream 中的一個中間操作,它接受一個 Predicate 參數,用于篩選出符合條件的元素。Predicate 是一個函數式接口,用于表示一個條件判斷操作。
使用 filter 操作篩選元素
java複制代碼import java.util.List;
import java.util.stream.Collectors;
public class StreamFilterExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = numbers.stream()
.filter(num -> num % 2 == 0)
.collect(Collectors.toList());
System.out.println("Even numbers: " + evenNumbers);
}
}
在上面的示例中,我們建立了一個整數清單 numbers,然後使用 stream 方法将其轉換為一個流。接着,我們使用 filter 操作篩選出偶數,最後通過 collect 方法将結果收集到一個新的清單中。
自定義篩選條件
你可以根據自己的需求定義不同的篩選條件。例如,篩選出字元串長度大于 5 的元素:
java複制代碼import java.util.List;
import java.util.stream.Collectors;
public class StreamFilterExample {
public static void main(String[] args) {
List<String> words = List.of("apple", "banana", "cherry", "date", "elderberry");
List<String> longWords = words.stream()
.filter(word -> word.length() > 5)
.collect(Collectors.toList());
System.out.println("Long words: " + longWords);
}
}
源碼解析
java複制代碼 @Override
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
// 判斷 predicate 是否為 null,如果為 null,則抛出 NullPointerException
Objects.requireNonNull(predicate);
/**
* 建構 Stream ,重寫 onWrapSink 方法
*/
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SIZED) {
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
/**
* 流已經建構完成,但是因為 begin 方法會優先執行,此時無法明确流中後續會存在多少元素,所有傳遞 -1,代表無法确定
*/
@Override
public void begin(long size) {
downstream.begin(-1);
}
//調用 predicate 中的 test 方法,進行條件判斷,最終将符合條件資料放入流中
@Override
public void accept(P_OUT u) {
if (predicate.test(u))
downstream.accept(u);
}
};
}
};
}
distinct
distinct 是一個中間操作,它可以應用于 Stream 上,用于消除流中的重複元素。
使用 distinct 操作去除重複元素
java複制代碼import java.util.List;
import java.util.stream.Collectors;
public class StreamDistinctExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 2, 3, 4, 4, 5, 5, 5);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("Distinct numbers: " + distinctNumbers);
}
}
在上面的示例中,我們建立了一個整數清單 numbers,其中包含重複的元素。使用 stream 方法将清單轉換為流,然後應用 distinct 操作消除重複元素。最終,通過 collect 方法将結果收集到一個新的清單中。
自定義去重邏輯
distinct 操作預設使用對象的 equals 方法來判斷是否為重複元素。
如果你希望根據自定義的邏輯來判斷元素是否重複,可以結合 equals 和 hashCode 方法的重寫,或者使用 Comparator 來自定義比較。
java複制代碼import java.util.List;
import java.util.stream.Collectors;
// 定義 Person 對象
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Person) {
return this.name.equals(((Person) obj).name);
}
return false;
}
@Override
public int hashCode() {
return this.name.hashCode();
}
}
public class StreamDistinctExample {
public static void main(String[] args) {
// 建立 Person 對象集合
List<Person> people = List.of(
new Person("Alice"),
new Person("Bob"),
new Person("Alice")
);
List<Person> distinctPeople = people.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("Distinct people: " + distinctPeople.stream().map(Person::getName).collect(Collectors.toList()));
}
}
切片操作
切片操作是 Java 8 中 Stream API 提供的一組操作,用于對流中的元素進行截取或分割,以擷取所需的部分元素。這組操作包括 limit、skip 和 substream(Java 9 及以上版本)。
limit
limit 操作是 Stream API 中的一個中間操作,它允許我們從流中擷取指定數量的元素,然後傳回一個新的流。這個操作在許多場景中都很有用,比如隻需要檢視前幾條記錄、分頁顯示資料等情況。
使用 limit 操作擷取前 N 個元素
java複制代碼public class LimitDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.limit(3)
.forEach(System.out::println);
}
}
在這個例子中,我們建立了一個包含整數的清單。我們首先将其轉換為流,然後使用 limit 方法隻擷取前三個元素。最後,我們使用 forEach 方法列印輸出結果。 過 limit 方法,我們限制了輸出結果的大小,隻擷取了前三個元素。除了以上的基本用法之外,limit 方法還可以和其他 Stream 方法一起使用,進行更複雜的操作。
源碼分析
java複制代碼 @Override
public final Stream<P_OUT> limit(long maxSize) {
if (maxSize < 0)
throw new IllegalArgumentException(Long.toString(maxSize));
return SliceOps.makeRef(this, 0, maxSize);
}
public static <T> Stream<T> makeRef(AbstractPipeline<?, T, ?> upstream,
long skip, long limit) {
if (skip < 0)
throw new IllegalArgumentException("Skip must be non-negative: " + skip);
return new ReferencePipeline.StatefulOp<T, T>(upstream, StreamShape.REFERENCE,
flags(limit)) {
Spliterator<T> unorderedSkipLimitSpliterator(Spliterator<T> s,
long skip, long limit, long sizeIfKnown) {
if (skip <= sizeIfKnown) {
// Use just the limit if the number of elements
// to skip is <= the known pipeline size
limit = limit >= 0 ? Math.min(limit, sizeIfKnown - skip) : sizeIfKnown - skip;
skip = 0;
}
return new StreamSpliterators.UnorderedSliceSpliterator.OfRef<>(s, skip, limit);
}
}
- 對于limit方法的實作,它會接收截取的長度,如果該值小于0,則抛出異常,否則會繼續向下調用SliceOps.makeRef()。
- 該方法中this代表目前流,skip代表需要跳過元素,比方說本來應該有4個元素,當跳過元素值為2,會跳過前面兩個元素,擷取後面兩個。maxSize代表要截取的長度.
- 在makeRef方法中的unorderedSkipLimitSpliterator()中接收了四個參數Spliterator,skip(跳過個數)、limit(截取個數)、sizeIfKnown(已知流大小)。如果跳過個數小于已知流大小,則判斷跳過個數是否大于0,如果大于則取截取個數或已知流大小-跳過個數的兩者最小值,否則取已知流大小-跳過個數的結果,作為跳過個數。 最後對集合基于跳過個數和截取個數進行切割。
skip
skip 操作是 Stream API 中的一個中間操作,它允許我們跳過流中的指定數量元素,然後傳回一個新的流。這個操作在許多場景中都很有用,比如分頁顯示資料、去除前幾條記錄等情況。
使用 skip 操作擷取前 N 個元素
java複制代碼public class SkipDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.skip(2)
.forEach(System.out::println);
}
}
使用 forEach 方法列印輸出結果。 過 limit 方法,我們限制了輸出結果的大小,隻擷取了前三個元素。除了以上的基本用法之外,limit 方法還可以和其他 Stream 方法一起使用,進行更複雜的操作。
映射操作
在Stream API中,映射操作是指将一個流中的每個元素都應用于一個函數,并将結果存儲在一個新的流中。這個函數可以是一個Lambda表達式,也可以是一個方法引用。映射操作可以用于将一個流中的元素轉化為另一種類型,或者提取出某個屬性或字段。映射操作就是将一個集合的每個元素應用某個函數,并将傳回值形成一個新的集合。
map
Stream接口中的map()方法是用于映射操作的主要方法之一。它接受一個Function函數作為參數,該函數将被應用于流中的每個元素,并傳回一個新的流。
使用 Map 操作
java複制代碼
public class MapDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
}
}
上述代碼中,map() 方法将集合中的每個元素進行平方計算,并将計算結果組成一個新的集合。
源碼分析
java複制代碼 public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
Objects.requireNonNull(mapper);
return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
return new Sink.ChainedReference<P_OUT, R>(sink) {
@Override
public void accept(P_OUT u) {
downstream.accept(mapper.apply(u));
}
};
}
};
}
将目前流給定函數中的元素,包含到一個新的流中進行傳回其會接收一個Function函數式接口,内部接收一個 内部對Function函數式接口中的apply方法進行實作,接收一個對象,傳回另外一個對象,并把這個内容存入目前流中,最後傳回。
flatMap
除了map()方法之外,Stream API還提供了flatMap()方法用于進行扁平化映射操作。它接受一個Function函數作為參數,該函數将被應用于流中的每個元素,并傳回一個新的流。不同之處在于,flatMap 操作在處理嵌套集合、展開多元資料結構等場景中非常有用。
使用 flatMap 操作
java複制代碼public class FlatMapDemo {
public static void main(String[] args) {
List<List<Integer>> nestedList = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
List<Integer> flattenedList = nestedList.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println(flattenedList); // 輸出 [1, 2, 3, 4, 5, 6, 7, 8, 9]
}
}
在上面的示例中,我們使用flatMap()方法将每個子清單轉換為流,然後合并這些流為一個扁平的流。
java複制代碼 public static void main(String[] args) {
List<String> words = Arrays.asList("Hello", "Stream", "API");
List<Character> characters = words.stream()
.flatMap(str -> str.chars().mapToObj(c -> (char) c))
.collect(Collectors.toList());
System.out.println(characters); // 輸出 [H, e, l, l, o, S, t, r, e, a, m, A, P, I]
}
在上述示例中,flatMap 操作将每個單詞轉換為字元流,然後将所有字元流合并為一個字元流。
比對操作
在日常開發中,有時還需要判斷集合中某些元素是否比對對應條件,如果有的話,在進行後續的操作。在Stream API中也提供了相關方法供我們進行使用,如anyMatch、allMatch等。他們對應的就是 && 和 || 運算符。
anyMatch
anyMatch 操作是 Stream API 中的一個終端操作,用于檢查流中是否至少有一個元素滿足給定的條件。當流中有任何一個元素滿足條件時,anyMatch 操作會傳回 true,否則傳回 false。并且對于它的操作,一般叫做短路求值
短路求值就是對于集合的一些操作,在正常情況下,無需處理整個集合就能得到結果,比方說通過 && 或者 || 連接配接一個判斷條件,對于流來說,某些操作不用操作整個流就能得到結果
使用 anyMatch 操作
java複制代碼public class AnyMatchDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3, 8, 4, 9, 2, 7);
boolean hasNumberGreaterThanFive = numbers.stream()
.anyMatch(number -> number > 5);
System.out.println(hasNumberGreaterThanFive); // 輸出 true
}
}
在上述示例中,anyMatch 操作用于檢查是否有元素大于 5,由于清單中存在 8、9 和 7,是以傳回結果為 true。
allMatch
allMatch 操作是 Stream API 中的一個終端操作,用于檢查流中的所有元素是否都滿足給定的條件。當流中的所有元素都滿足條件時,allMatch 操作會傳回 true,否則傳回 false。
使用 allMatch 操作
java複制代碼 public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
boolean allWordsHaveLengthThreeOrMore = words.stream()
.allMatch(word -> word.length() >= 3);
System.out.println(allWordsHaveLengthThreeOrMore); // 輸出 true
}
在上述示例中,allMatch 操作用于檢查是否所有字元串的長度都大于等于 3,由于清單中的所有字元串都滿足條件,是以傳回結果為 true。
查找操作
在日常開發中,有時候還需要從集合中查找符合條件的元素,Stream 也提供了相關的 API,如 findAny 和 findFirst 等方法。同時上述方法也可以跟其他流操作組合使用
findAny
findAny 操作是 Stream API 中的一個終端操作,它用于在流中查找任意一個滿足給定條件的元素。由于流可能是并行處理的,是以傳回的是一個可能滿足條件的元素,而不是第一個元素
java複制代碼
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
Optional<String> anyWordStartingWithA = words.stream()
.filter(word -> word.startsWith("a"))
.findAny();
if (anyWordStartingWithA.isPresent()) {
System.out.println("Found: " + anyWordStartingWithA.get()); // 輸出 Found: apple 或其他以 "a" 開頭的單詞
} else {
System.out.println("Not found");
}
}
在上述示例中,filter 操作用于過濾以字母 "a" 開頭的單詞,然後使用 findAny 操作來查找任意一個滿足條件的單詞。
findFirst
findFirst 操作是 Stream API 中的一個終端操作,它用于在流中查找第一個滿足給定條件的元素。由于流可能是并行處理的,是以傳回的是第一個滿足條件的元素。
使用 findFirst 操作
java複制代碼 public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");
Optional<String> firstWordStartingWithA = words.stream()
.filter(word -> word.startsWith("a"))
.findFirst();
if (firstWordStartingWithA.isPresent()) {
System.out.println("Found: " + firstWordStartingWithA.get()); // 輸出 Found: apple
} else {
System.out.println("Not found");
}
}
在上述示例中,filter 操作用于過濾以字母 "a" 開頭的單詞,然後使用 findFirst 操作來查找第一個滿足條件的單詞。
歸約操作
歸約操作是将一個流中的元素按照給定的操作進行合并,得到一個最終的結果。歸約操作的基本思想是将流中的元素逐個進行合并,最終得到一個合并後的結果。
在 Stream API 中,reduce 方法用于實作歸約操作。它有兩種重載形式:
- Optional<T> reduce(BinaryOperator<T> accumulator)
- T reduce(T identity, BinaryOperator<T> accumulator)
其中,accumulator 是一個二進制操作,用于定義合并規則。identity 是一個初始值,用于在歸約開始前初始化結果。
使用歸約操作
示例 1:對整數清單求和
假設我們有一個整數清單,現在我們想要計算清單中所有元素的和。
ini複制代碼javaCopy code
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); // 輸出 Sum: 15
在上述示例中,reduce 操作用于計算整數清單中所有元素的和。
示例 2:連接配接字元串清單
假設我們有一個字元串清單,現在我們想要将清單中的所有字元串連接配接成一個大字元串。
ini複制代碼javaCopy code
List<String> words = Arrays.asList("Hello", "Stream", "API");
String concatenated = words.stream()
.reduce("", (a, b) -> a + " " + b);
System.out.println("Concatenated: " + concatenated); // 輸出 Concatenated: Hello Stream API
在上述示例中,reduce 操作用于将字元串清單中的所有字元串連接配接成一個大字元串。
示例 3:求最大值
假設我們有一個整數清單,現在我們想要找到清單中的最大值。
ini複制代碼javaCopy code
List<Integer> numbers = Arrays.asList(5, 9, 3, 7, 1);
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(value -> System.out.println("Max: " + value)); // 輸出 Max: 9
在上述示例中,reduce 操作用于找到整數清單中的最大值。
總結
Stream 是 Java 8 中引入的一個強大的功能,它提供了一種更簡潔、更具有表現力的方式來處理集合資料。通過使用 Stream,我們可以輕松地進行過濾、映射、排序、歸約等操作,進而實作更加優雅和函數式的代碼編寫。