還看不懂同事的程式碼?超強的 Stream 流操作姿勢還不學習一下
Java 8 新特性系列文章索引。
- Jdk14都要出了,還不能使用 Optional優雅的處理空指標?
- Jdk14 都要出了,Jdk8 的時間處理姿勢還不瞭解一下?
- 還看不懂同事的程式碼?Lambda 表示式、函式介面瞭解一下
前言
我們都知道 Lambda 和 Stream 是 Java 8 的兩大亮點功能,在前面的文章裡已經介紹過 Lambda 相關知識,這次介紹下 Java 8 的 Stream 流操作。它完全不同於 java.io 包的 Input/Output Stream ,也不是大資料實時處理的 Stream 流。這個 Stream 流操作是 Java 8 對集合操作功能的增強,專注於對集合的各種高效、便利、優雅的聚合操作。藉助於 Lambda 表示式,顯著的提高程式設計效率和可讀性。且 Stream 提供了平行計算模式,可以簡潔的編寫出並行程式碼,能充分發揮如今計算機的多核處理優勢。
在使用 Stream 流操作之前你應該先了解 Lambda 相關知識,如果還不瞭解,可以參考之前文章:還看不懂同事的程式碼?Lambda 表示式、函式介面瞭解一下 。
1. Stream 流介紹
Stream 不同於其他集合框架,它也不是某種資料結構,也不會儲存資料,但是它負責相關計算,使用起來更像一個高階的迭代器。在之前的迭代器中,我們只能先遍歷然後在執行業務操作,而現在只需要指定執行什麼操作, Stream 就會隱式的遍歷然後做出想要的操作。另外 Stream 和迭代器一樣的只能單向處理,如同奔騰長江之水一去而不復返。
由於 Stream 流提供了惰性計算和並行處理的能力,在使用平行計算方式時資料會被自動分解成多段然後並行處理,最後將結果彙總。所以 Stream 操作可以讓程式執行變得更加高效。
2. Stream 流概念
Stream 流的使用總是按照一定的步驟進行,可以抽象出下面的使用流程。
資料來源(source) -> 資料處理/轉換(intermedia) -> 結果處理(terminal )
2.1. 資料來源
資料來源(source)
也就是資料的來源,可以通過多種方式獲得 Stream 資料來源,下面列舉幾種常見的獲取方式。
- Collection.stream(); 從集合獲取流。
- Collection.parallelStream(); 從集合獲取並行流。
- Arrays.stream(T array) or Stream.of(); 從陣列獲取流。
- BufferedReader.lines(); 從輸入流中獲取流。
- IntStream.of() ; 從靜態方法中獲取流。
- Stream.generate(); 自己生成流
2.2. 資料處理
資料處理/轉換(intermedia)
步驟可以有多個操作,這步也被稱為intermedia
(中間操作)。在這個步驟中不管怎樣操作,它返回的都是一個新的流物件,原始資料不會發生任何改變,而且這個步驟是惰性計算
處理的,也就是說只調用方法並不會開始處理,只有在真正的開始收集結果時,中間操作才會生效,而且如果遍歷沒有完成,想要的結果已經獲取到了(比如獲取第一個值),會停止遍歷,然後返回結果。惰性計算
可以顯著提高執行效率。
資料處理演示。
@Test
public void streamDemo(){
List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter");
// 1. 篩選出名字長度為4的
// 2. 名字前面拼接 This is
// 3. 遍歷輸出
nameList.stream()
.filter(name -> name.length() == 4)
.map(name -> "This is "+name)
.forEach(name -> System.out.println(name));
}
// 輸出結果
// This is Jack
// This is Poul
資料處理/轉換
操作自然不止是上面演示的過濾 filter
和 map
對映兩種,另外還有 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等。
2.3. 收集結果
結果處理(terminal )
是流處理的最後一步,執行完這一步之後流會被徹底用盡,流也不能繼續操作了。也只有到了這個操作的時候,流的資料處理/轉換
等中間過程才會開始計算,也就是上面所說的惰性計算
。結果處理
也必定是流操作的最後一步。
常見的結果處理
操作有 forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator 等。
下面演示了簡單的結果處理
的例子。
/**
* 轉換成為大寫然後收集結果,遍歷輸出
*/
@Test
public void toUpperCaseDemo() {
List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter");
List<String> upperCaseNameList = nameList.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
upperCaseNameList.forEach(name -> System.out.println(name + ","));
}
// 輸出結果
// DARCY,CHRIS,LINDA,SID,KIM,JACK,POUL,PETER,
2.4. short-circuiting
有一種 Stream 操作被稱作 short-circuiting
,它是指當 Stream 流無限大但是需要返回的 Stream 流是有限的時候,而又希望它能在有限的時間內計算出結果,那麼這個操作就被稱為short-circuiting
。例如 findFirst
操作。
3. Stream 流使用
Stream 流在使用時候總是藉助於 Lambda 表示式進行操作,Stream 流的操作也有很多種方式,下面列舉的是常用的 11 種操作。
3.1. Stream 流獲取
獲取 Stream 的幾種方式在上面的 Stream 資料來源裡已經介紹過了,下面是針對上面介紹的幾種獲取 Stream 流的使用示例。
@Test
public void createStream() throws FileNotFoundException {
List<String> nameList = Arrays.asList("Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter");
String[] nameArr = {"Darcy", "Chris", "Linda", "Sid", "Kim", "Jack", "Poul", "Peter"};
// 集合獲取 Stream 流
Stream<String> nameListStream = nameList.stream();
// 集合獲取並行 Stream 流
Stream<String> nameListStream2 = nameList.parallelStream();
// 陣列獲取 Stream 流
Stream<String> nameArrStream = Stream.of(nameArr);
// 陣列獲取 Stream 流
Stream<String> nameArrStream1 = Arrays.stream(nameArr);
// 檔案流獲取 Stream 流
BufferedReader bufferedReader = new BufferedReader(new FileReader("README.md"));
Stream<String> linesStream = bufferedReader.lines();
// 從靜態方法獲取流操作
IntStream rangeStream = IntStream.range(1, 10);
rangeStream.limit(10).forEach(num -> System.out.print(num+","));
System.out.println();
IntStream intStream = IntStream.of(1, 2, 3, 3, 4);
intStream.forEach(num -> System.out.print(num+","));
}
3.2. forEach
forEach
是 Strean 流中的一個重要方法,用於遍歷 Stream 流,它支援傳入一個標準的 Lambda 表示式。但是它的遍歷不能通過 return/break 進行終止。同時它也是一個 terminal
操作,執行之後 Stream 流中的資料會被消費掉。
如輸出物件。
List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numberList.stream().forEach(number -> System.out.println(number+","));
// 輸出結果
// 1,2,3,4,5,6,7,8,9,
3.3. map / flatMap
使用 map
把物件一對一對映成另一種物件或者形式。
/**
* 把數字值乘以2
*/
@Test
public void mapTest() {
List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// 對映成 2倍數字
List<Integer> collect = numberList.stream()
.map(number -> number * 2)
.collect(Collectors.toList());
collect.forEach(number -> System.out.print(number + ","));
System.out.println();
numberList.stream()
.map(number -> "數字 " + number + ",")
.forEach(number -> System.out.println(number));
}
// 輸出結果
// 2,4,6,8,10,12,14,16,18,
// 數字 1,數字 2,數字 3,數字 4,數字 5,數字 6,數字 7,數字 8,數字 9,
上面的 map
可以把資料進行一對一的對映,而有些時候關係可能不止 1對 1那麼簡單,可能會有1對多。這時可以使用 flatMap。下面演示
使用 flatMap
把物件扁平化展開。
/**
* flatmap把物件扁平化
*/
@Test
public void flatMapTest() {
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
List<Integer> collect = inputStream
.flatMap((childList) -> childList.stream())
.collect(Collectors.toList());
collect.forEach(number -> System.out.print(number + ","));
}
// 輸出結果
// 1,2,3,4,5,6,
3.4. filter
使用 filter
進行資料篩選,挑選出想要的元素,下面的例子演示怎麼挑選出偶數數字。
/**
* filter 資料篩選
* 篩選出偶數數字
*/
@Test
public void filterTest() {
List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> collect = numberList.stream()
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());
collect.forEach(number -> System.out.print(number + ","));
}
得到如下結果。
2,4,6,8,
3.5. findFirst
findFirst
可以查找出 Stream 流中的第一個元素,它返回的是一個 Optional 型別,如果還不知道 Optional 類的用處,可以參考之前文章 Jdk14都要出了,還不能使用 Optional優雅的處理空指標? 。
/**
* 查詢第一個資料
* 返回的是一個 Optional 物件
*/
@Test
public void findFirstTest(){
List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Optional<Integer> firstNumber = numberList.stream()
.findFirst();
System.out.println(firstNumber.orElse(-1));
}
// 輸出結果
// 1
findFirst
方法在查詢到需要的資料之後就會返回不再遍歷資料了,也因此 findFirst
方法可以對有無限資料的 Stream 流進行操作,也可以說 findFirst
是一個 short-circuiting
操作。
3.6. collect / toArray
Stream 流可以輕鬆的轉換為其他結構,下面是幾種常見的示例。
/**
* Stream 轉換為其他資料結構
*/
@Test
public void collectTest() {
List<Integer> numberList = Arrays.asList(1, 1, 2, 2, 3, 3, 4, 4, 5);
// to array
Integer[] toArray = numberList.stream()
.toArray(Integer[]::new);
// to List
List<Integer> integerList = numberList.stream()
.collect(Collectors.toList());
// to set
Set<Integer> integerSet = numberList.stream()
.collect(Collectors.toSet());
System.out.println(integerSet);
// to string
String toString = numberList.stream()
.map(number -> String.valueOf(number))
.collect(Collectors.joining()).toString();
System.out.println(toString);
// to string split by ,
String toStringbJoin = numberList.stream()
.map(number -> String.valueOf(number))
.collect(Collectors.joining(",")).toString();
System.out.println(toStringbJoin);
}
// 輸出結果
// [1, 2, 3, 4, 5]
// 112233445
// 1,1,2,2,3,3,4,4,5
3.7. limit / skip
獲取或者扔掉前 n 個元素
/**
* 獲取 / 扔掉前 n 個元素
*/
@Test
public void limitOrSkipTest() {
// 生成自己的隨機數流
List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26);
ageList.stream()
.limit(3)
.forEach(age -> System.out.print(age+","));
System.out.println();
ageList.stream()
.skip(3)
.forEach(age -> System.out.print(age+","));
}
// 輸出結果
// 11,22,13,
// 14,25,26,
3.8. Statistics
數學統計功能,求一組陣列的最大值、最小值、個數、資料和、平均數等。
/**
* 數學計算測試
*/
@Test
public void mathTest() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
IntSummaryStatistics stats = list.stream().mapToInt(x -> x).summaryStatistics();
System.out.println("最小值:" + stats.getMin());
System.out.println("最大值:" + stats.getMax());
System.out.println("個數:" + stats.getCount());
System.out.println("和:" + stats.getSum());
System.out.println("平均數:" + stats.getAverage());
}
// 輸出結果
// 最小值:1
// 最大值:6
// 個數:6
// 和:21
// 平均數:3.5
3.9. groupingBy
分組聚合功能,和資料庫的 Group by 的功能一致。
/**
* groupingBy
* 按年齡分組
*/
@Test
public void groupByTest() {
List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26);
Map<String, List<Integer>> ageGrouyByMap = ageList.stream()
.collect(Collectors.groupingBy(age -> String.valueOf(age / 10)));
ageGrouyByMap.forEach((k, v) -> {
System.out.println("年齡" + k + "0多歲的有:" + v);
});
}
// 輸出結果
// 年齡10多歲的有:[11, 13, 14]
// 年齡20多歲的有:[22, 25, 26]
3.10. partitioningBy
/**
* partitioningBy
* 按某個條件分組
* 給一組年齡,分出成年人和未成年人
*/
public void partitioningByTest() {
List<Integer> ageList = Arrays.asList(11, 22, 13, 14, 25, 26);
Map<Boolean, List<Integer>> ageMap = ageList.stream()
.collect(Collectors.partitioningBy(age -> age > 18));
System.out.println("未成年人:" + ageMap.get(false));
System.out.println("成年人:" + ageMap.get(true));
}
// 輸出結果
// 未成年人:[11, 13, 14]
// 成年人:[22, 25, 26]
3.11. 進階 - 自己生成 Stream 流
/**
* 生成自己的 Stream 流
*/
@Test
public void generateTest(){
// 生成自己的隨機數流
Random random = new Random();
Stream<Integer> generateRandom = Stream.generate(random::nextInt);
generateRandom.limit(5).forEach(System.out::println);
// 生成自己的 UUID 流
Stream<UUID> generate = Stream.generate(UUID::randomUUID);
generate.limit(5).forEach(System.out::println);
}
// 輸出結果
// 793776932
// -2051545609
// -917435897
// 298077102
// -1626306315
// 31277974-841a-4ad0-a809-80ae105228bd
// f14918aa-2f94-4774-afcf-fba08250674c
// d86ccefe-1cd2-4eb4-bb0c-74858f2a7864
// 4905724b-1df5-48f4-9948-fa9c64c7e1c9
// 3af2a07f-0855-455f-a339-6e890e533ab3
上面的例子中 Stream 流是無限的,但是獲取到的結果是有限的,使用了 Limit
限制獲取的數量,所以這個操作也是 short-circuiting
操作。
4. Stream 流優點
4.1. 簡潔優雅
正確使用並且正確格式化的 Stream 流操作程式碼不僅簡潔優雅,更讓人賞心悅目。下面對比下在使用 Stream 流和不使用 Stream 流時相同操作的編碼風格。
/**
* 使用流操作和不使用流操作的編碼風格對比
*/
@Test
public void diffTest() {
// 不使用流操作
List<String> names = Arrays.asList("Jack", "Jill", "Nate", "Kara", "Kim", "Jullie", "Paul", "Peter");
// 篩選出長度為4的名字
List<String> subList = new ArrayList<>();
for (String name : names) {
if (name.length() == 4) {
subList.add(name);
}
}
// 把值用逗號分隔
StringBuilder sbNames = new StringBuilder();
for (int i = 0; i < subList.size() - 1; i++) {
sbNames.append(subList.get(i));
sbNames.append(", ");
}
// 去掉最後一個逗號
if (subList.size() > 1) {
sbNames.append(subList.get(subList.size() - 1));
}
System.out.println(sbNames);
}
// 輸出結果
// Jack, Jill, Nate, Kara, Paul
如果是使用 Stream 流操作。
// 使用 Stream 流操作
String nameString = names.stream()
.filter(num -> num.length() == 4)
.collect(Collectors.joining(", "));
System.out.println(nameString);
4.2. 惰性計算
上面有提到,資料處理/轉換(intermedia)
操作 map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered 等這些操作,在呼叫方法時並不會立即呼叫,而是在真正使用的時候才會生效,這樣可以讓操作延遲到真正需要使用的時刻。
下面會舉個例子演示這一點。
/**
* 找出偶數
*/
@Test
public void lazyTest() {
// 生成自己的隨機數流
List<Integer> numberLIst = Arrays.asList(1, 2, 3, 4, 5, 6);
// 找出偶數
Stream<Integer> integerStream = numberLIst.stream()
.filter(number -> {
int temp = number % 2;
if (temp == 0 ){
System.out.println(number);
}
return temp == 0;
});
System.out.println("分割線");
List<Integer> collect = integerStream.collect(Collectors.toList());
}
如果沒有 惰性計算
,那麼很明顯會先輸出偶數,然後輸出 分割線
。而實際的效果是。
分割線
2
4
6
可見 惰性計算
把計算延遲到了真正需要的時候。
4.3. 平行計算
獲取 Stream 流時可以使用 parallelStream
方法代替 stream
方法以獲取並行處理流,並行處理可以充分的發揮多核優勢,而且不增加編碼的複雜性。
下面的程式碼演示了生成一千萬個隨機數後,把每個隨機數乘以2然後求和時,序列計算和平行計算的耗時差異。
/**
* 平行計算
*/
@Test
public void main() {
// 生成自己的隨機數流,取一千萬個隨機數
Random random = new Random();
Stream<Integer> generateRandom = Stream.generate(random::nextInt);
List<Integer> numberList = generateRandom.limit(10000000).collect(Collectors.toList());
// 序列 - 把一千萬個隨機數,每個隨機數 * 2 ,然後求和
long start = System.currentTimeMillis();
int sum = numberList.stream()
.map(number -> number * 2)
.mapToInt(x -> x)
.sum();
long end = System.currentTimeMillis();
System.out.println("序列耗時:"+(end - start)+"ms,和是:"+sum);
// 並行 - 把一千萬個隨機數,每個隨機數 * 2 ,然後求和
start = System.currentTimeMillis();
sum = numberList.parallelStream()
.map(number -> number * 2)
.mapToInt(x -> x)
.sum();
end = System.currentTimeMillis();
System.out.println("並行耗時:"+(end - start)+"ms,和是:"+sum);
}
得到如下輸出。
序列耗時:1005ms,和是:481385106
並行耗時:47ms,和是:481385106
效果顯而易見,程式碼簡潔優雅。
5. Stream 流建議
5.1 保證正確排版
從上面的使用案例中,可以發現使用 Stream 流操作的程式碼非常簡潔,而且可讀性更高。但是如果不正確的排版,那麼看起來將會很糟糕,比如下面的同樣功能的程式碼例子,多幾層操作呢,是不是有些讓人頭大?
// 不排版
String string = names.stream().filter(num -> num.length() == 4).map(name -> name.toUpperCase()).collect(Collectors.joining(","));
// 排版
String string = names.stream()
.filter(num -> num.length() == 4)
.map(name -> name.toUpperCase())
.collect(Collectors.joining(","));
5.1 保證函式純度
如果想要你的 Stream 流對於每次的相同操作的結果都是相同的話,那麼你必須保證 Lambda 表示式的純度,也就是下面亮點。
- Lambda 中不會更改任何元素。
- Lambda 中不依賴於任何可能更改的元素。
這兩點對於保證函式的冪等非常重要,不然你程式執行結果可能會變得難以預測,就像下面的例子。
@Test
public void simpleTest(){
List<Integer> numbers = Arrays.asList(1, 2, 3);
int[] factor = new int[] { 2 };
Stream<Integer> stream = numbers.stream()
.map(e -> e * factor[0]);
factor[0] = 0;
stream.forEach(System.out::println);
}
// 輸出結果
// 0
// 0
// 0
文中程式碼都已經上傳到
https://github.com/niumoo/jdk-feature/blob/master/src/main/java/net/codingme/feature/jdk8/Jdk8Stream.java。
<完>
個人網站:https://www.codingme.net
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回覆資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試資料。