成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

小小 Stream,一篇文章拿捏它

開發 前端
在之前的 Java 中的 Lambda 文章中,我簡要提到了 Stream 的使用。在這篇文章中將深入探討它。

在之前的 Java 中的 Lambda文章中,我簡要提到了 Stream 的使用。在這篇文章中將深入探討它。首先,我們以一個熟悉的Student類為例。假設有一組學生:

public class Student {
    private String name;
    private Integer age;

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
    // toString 方法
    @Override
    public String toString() {
        return"Student{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}
List<Student> students = new ArrayList<>();
students.add(new Student("Bob", 18));
students.add(new Student("Ted", 17));
students.add(new Student("Zeka", 19));

現在有這樣一個需求:從給定的學生列表中返回年齡大于等于 18 歲的學生,按年齡降序排列,最多返回 2 個。

在Java7 及更早的代碼中,我們會這樣實現:

public static List<Student> getTwoOldestStudents(List<Student> students) { 
    List<Student> result = new ArrayList<>(); 
    // 1. 遍歷學生列表,篩選出符合年齡條件的學生
    for (Student student : students) { 
        if (student.getAge() >= 18) { 
            result.add(student); 
        } 
    } 
    // 2. 對符合條件的學生按年齡排序
    result.sort((s1, s2) -> s2.getAge() - s1.getAge()); 
    // 3. 如果結果大于 2 個,截取前兩個數據并返回
    if (result.size() > 2) { 
        result = result.subList(0, 2); 
    } 
    return result; 
}

在Java8 及以后的版本中,借助 Stream,我們可以更優雅地寫出以下代碼:

public static List<Student> getTwoOldestStudentsByStream(List<Student> students) {
    return students.stream()
        .filter(s -> s.getAge() >= 18)
        .sorted((s1, s2) -> s2.getAge() - s1.getAge())
        .limit(2)
        .collect(Collectors.toList());
}

兩種方法的區別:

  • 從功能角度來看,過程式代碼實現將集合元素、循環迭代和各種邏輯判斷耦合在一起,暴露了太多細節。隨著需求的變化和復雜化,過程式代碼將變得難以理解和維護。
  • 函數式解決方案將代碼細節和業務邏輯解耦。類似于 SQL 語句,它表達的是“做什么”而不是“怎么做”,讓程序員更專注于業務邏輯,寫出更簡潔、易理解和維護的代碼。

基于我日常項目的實踐經驗,我對 Stream 的核心點、易混淆的用法、典型使用場景等做了詳細總結。希望能幫助大家更全面地理解 Stream,并在項目開發中更高效地應用它。

一、初識 Stream

Java 8 新增了 Stream 特性,它使用戶能夠以函數式且更簡單的方式操作 List、Collection 等數據結構,并在用戶無感知的情況下實現并行計算。

簡而言之,Stream 操作被組合成一個 Stream 管道。Stream 管道由以下三部分組成:

  • 創建 Stream(從源數據創建,源數據可以是數組、集合、生成器函數、I/O 通道等);
  • 中間操作(可能有零個或多個,它們將一個 Stream 轉換為另一個 Stream,例如filter(Predicate));
  • 終止操作(產生結果從而終止 Stream,例如count()或forEach(Consumer))。

下圖展示了這些過程:

每個階段里的 Stream 操作都包含多個方法。我們先來簡單了解下每個方法的功能。

1. 創建 Stream

主要負責直接創建一個新的 Stream,或基于現有的數組、List、Set、Map 等集合類型對象創建新的 Stream。

API

解釋

stream()

創建一個新的串行流對象

parallelStream()

創建一個可以并行執行的流對象

Stream.of()

從給定的元素序列創建一個新的串行流對象

除了Stream,還有IntStream、LongStream和DoubleStream等基本類型的流,它們都稱為“流”。

2. 中間操作

這一步負責處理 Stream 并返回一個新的 Stream 對象。中間操作可以疊加。

API

解釋

filter()

過濾符合條件的元素并返回一個新的流

sorted()

按指定規則對所有元素排序并返回一個新的流

skip()

跳過集合前面的指定數量的元素并返回一個新的流

distinct()

去重并返回一個新的流

limit()

只保留集合前面的指定數量的元素并返回一個新的流

concat()

將兩個流的數據合并為一個新的流并返回

peek()

遍歷并處理流中的每個元素并返回處理后的流

map()

將現有元素轉換為另一種對象類型(一對一)并返回一個新的流

flatMap()

將現有元素轉換為另一種對象類型(一對多),即一個原始元素對象可能轉換為一個或多個新類型的元素,然后返回一個新的流

3. 終止操作

顧名思義,終止操作后 Stream 將結束,最后可能會執行一些邏輯處理,或根據需求返回一些執行結果。

API

解釋

findFirst()

找到第一個符合條件的元素時終止流處理

findAny()

找到任意一個符合條件的元素時終止流處理

anyMatch()

返回布爾值,類似于isContains(),用于判斷是否有符合條件的元素

allMatch()

返回布爾值,用于判斷是否所有元素都符合條件

noneMatch()

返回布爾值,用于判斷是否所有元素都不符合條件

min()

返回流處理后的最小值

max()

返回流處理后的最大值

count()

返回流處理后的元素數量

collect()

將流轉換為指定類型,通過Collectors指定

toArray()

將流轉換為數組

iterator()

將流轉換為迭代器對象

forEach()

無返回值,遍歷元素并執行給定的處理邏輯

二、代碼實戰

1. 創建 Stream

// Stream.of, IntStream.of...
Stream<String> nameStream = Stream.of("Bob", "Ted", "Zeka");
IntStream ageStream = IntStream.of(18, 17, 19);

// stream, parallelStream
Stream<Student> studentStream = students.stream();
Stream<Student> studentParallelStream = students.parallelStream();

在大多數情況下,我們基于現有的集合創建 Stream。

2. 中間操作

(1) map

map和flatMap都用于將現有元素轉換為其他類型。區別在于:

  • map必須是一對一的,即每個元素只能轉換為一個新元素;
  • flatMap可以是一對多的,即每個元素可以轉換為一個或多個新元素。

我們先來看map方法。當前需求如下:將之前的學生對象列表轉換為學生姓名列表并輸出:

public static List<String> objectToString(List<Student> students) {
    return students.stream()
        .map(Student::getName)
        .collect(Collectors.toList());
}

輸出:

[Bob, Ted, Zeka]

可以看到,輸入中有三個學生,輸出也是三個學生姓名。

(2) flatMap

學校要求每個學生加入一個團隊。假設 Bob、Ted 和 Zeka 加入了籃球隊,Alan、Anne 和 Davis 加入了足球隊。

public class Team {
    private String type;
    private List<Student> students;

    public Team(String type, List<Student> students) {
        this.type = type;
        this.students = students;
    }

    public String getType() {
        return type;
    }

    public List<Student> getStudents() {
        return students;
    }
@Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Team{");
        sb.append("type='").append(type).append('\'');
        sb.append(", students=[");
        for (int i = 0; i < students.size(); i++) {
            Student student = students.get(i);
            sb.append("{name='").append(student.getName()).append("', age=").append(student.getAge()).append('}');
            if (i < students.size() - 1) {
                sb.append(", ");
            }
        }
        sb.append("]}");
        return sb.toString();
    }
}
List<Student> basketballStudents = new ArrayList<>();
basketballStudents.add(new Student("Bob", 18));
basketballStudents.add(new Student("Ted", 17));
basketballStudents.add(new Student("Zeka", 19));

List<Student> footballStudents = new ArrayList<>();
footballStudents.add(new Student("Alan", 19));
footballStudents.add(new Student("Anne", 21));
footballStudents.add(new Student("Davis", 21));

Team basketballTeam = new Team("basketball", basketballStudents);
Team footballTeam = new Team("football", footballStudents);

List<Team> teams = new ArrayList<>();
teams.add(basketballTeam);
teams.add(footballTeam);

現在我們需要統計所有團隊中的學生,并將他們合并到一個列表中。你會如何實現這個需求?

在 Java7 及更早的版本中可以通過以下方式解決:

List<Student> allStudents = new ArrayList<>();
for (Team team : teams) {
    for (Student student : team.getStudents()) {
        allStudents.add(student);
    }
}

但這段代碼有兩個嵌套的 for 循環,不夠優雅。面對這個需求,flatMap可以派上用場。

List<Student> allStudents = teams.stream()
    .flatMap(t -> t.getStudents().stream())
    .collect(Collectors.toList());

一行代碼就搞定了。flatMap方法接受一個 lambda 表達式函數,函數的返回值必須是一個 Stream 類型。flatMap方法最終會將所有返回的 Stream 合并生成一個新的 Stream,而map方法無法做到。

下圖清晰地展示了flatMap的處理邏輯:

(3) filter, distinct, sorted, limit

關于剛才所有團隊中的學生列表,我們現在需要知道這些學生中第二和第三大的年齡。他們必須至少 18 歲。此外,如果有重復的年齡,只能算一個。

List<Integer> topTwoAges = allStudents.stream()
    .map(Student::getAge) // [18, 17, 19, 19, 21, 21]
    .filter(a -> a >= 18) // [18, 19, 19, 21, 21]
    .distinct() // [18, 19, 21]
    .sorted((a1, a2) -> a2 - a1) // [21, 19, 18]
    .skip(1) // [19, 18]
    .limit(2) // [19, 18]
    .collect(Collectors.toList());
System.out.println(topTwoAges);

輸出:

[19, 18]

注意:由于在skip方法操作后只剩下兩個元素,limit步驟實際上可以省略。

(4) peek, foreach

peek方法和foreach方法都可以用于遍歷元素并逐個處理,因此我們將它們放在一起進行比較和講解。但值得注意的是,peek是一個中間操作方法,而foreach是一個終止操作方法。

中間操作只能作為 Stream 管道中間的處理步驟,不能直接執行以獲取結果,必須與終止操作配合執行。而foreach作為一個沒有返回值的終止方法,可以直接執行相應的操作。

比如,我們分別使用peek和foreach對籃球隊的每個學生說“Hello, xxx…”。

// peek
System.out.println("---start peek---");
basketballTeam.getStudents().stream().peek(s -> System.out.println("Hello, " + s.getName()));
System.out.println("---end peek---");

// foreach
System.out.println("---start foreach---");
basketballTeam.getStudents().stream().forEach(s -> System.out.println("Hello, " + s.getName()));
System.out.println("---end foreach---");

從輸出中可以看出,peek在單獨調用時不會執行,而foreach可以直接執行:

---start peek---
---end peek---
---start foreach---
Hello, Bob
Hello, Ted
Hello, Zeka
---end foreach---

如果在peek后面加上終止操作,它就可以執行。

System.out.println("---start peek---");
basketballTeam.getStudents().stream().peek(s -> System.out.println("Hello, " + s.getName())).count();
System.out.println("---end peek---");

// 輸出
---start peek---
Hello, Bob
Hello, Ted
Hello, Zeka
---end peek---

peek應謹慎用于業務處理邏輯。因為peek方法是否執行在各個版本并不一致。

例如,在 Java8 版本中,剛才的peek方法會正常執行,但在 Java17 中,它會被自動優化,peek中的邏輯不會執行。至于原因,你可以查看 JDK17 的官方 API 文檔。

三、終止操作

根據終止操作返回的結果類型大概分為兩類。

一類返回的是簡單類型,主要包括max、min、count、findAny、findFirst、anyMatch、allMatch等方法。

另一類是返回的是集合類型。大多數場景是獲取集合類的結果對象,如 List、Set 或 HashMap 等,主要通過collect方法實現。

1. 簡單結果類型

(1) max, min

max()和min()主要用于返回流處理后元素的最大值/最小值。返回結果由Optional包裝。關于Optional的使用,請參考之前的Java 中如何優雅地處理 null 值文章 。

我們直接看例子:

找到足球隊中年齡最大和最小的是誰?

// max
footballTeam.getStudents().stream()
    .map(Student::getAge)
    .max(Comparator.comparing(a -> a))
    .ifPresent(a -> System.out.println("足球隊中最大的年齡是:" + a));

// min
footballTeam.getStudents().stream()
    .map(Student::getAge)
    .min(Comparator.comparing(a -> a))
    .ifPresent(a -> System.out.println("足球隊中最小的年齡是:" + a));

輸出:

足球隊中最大的年齡是:21
足球隊中最小的年齡是:19

(2) findAny, findFirst

findAny()和findFirst()主要用于在找到符合條件的元素。對于串行 Stream,findAny()和findFirst()功能相同;對于并行 Stream,findAny()更高效。

假設籃球隊新增了一個學生 Tom,年齡為 19 歲。

List<Student> basketballStudents = new ArrayList<>();
basketballStudents.add(new Student("Bob", 18));
basketballStudents.add(new Student("Ted", 17));
basketballStudents.add(new Student("Zeka", 19));
basketballStudents.add(new Student("Tom", 19));

現在需要查找到:

  • 籃球隊中第一個年齡為 19 歲的學生姓名;
  • 籃球隊中任意一個年齡為 19 歲的學生姓名。
// findFirst
basketballStudents.stream()
    .filter(s -> s.getAge() == 19)
    .findFirst()
    .map(Student::getName)
    .ifPresent(name -> System.out.println("findFirst: " + name));

// findAny
basketballStudents.stream()
    .filter(s -> s.getAge() == 19)
    .findAny()
    .map(Student::getName)
    .ifPresent(name -> System.out.println("findAny: " + name));

輸出:

findFirst: Zeka
findAny: Zeka

可以看到,在串行 Stream 下,這兩個功能沒有區別。并行處理的區別將在后面介紹。

(3) count

籃球隊新增了一個學生,現在籃球隊有多少學生?

System.out.println("籃球隊的學生人數:" + basketballStudents.stream().count());

輸出:

籃球隊的學生人數:4

(4) anyMatch, allMatch, noneMatch

顧名思義,這三個方法用于判斷元素是否符合條件,并返回布爾值。看以下三個例子:

  • 足球隊中是否有名為 Alan 的學生?
  • 足球隊中的所有學生是否都小于 22 歲?
  • 足球隊中是否沒有年齡超過 20 歲的學生?
// anyMatch
System.out.println("anyMatch: " + footballStudents.stream().anyMatch(s -> s.getName().equals("Alan")));

// allMatch
System.out.println("allMatch: " + footballStudents.stream().allMatch(s -> s.getAge() < 22));

// noneMatch
System.out.println("noneMatch: " + footballStudents.stream().noneMatch(s -> s.getAge() > 20));

輸出:

anyMatch: true
allMatch: true
noneMatch: false

2. 結果集合類型

(1) 生成集合

生成集合應該是collect最常用的場景。除了之前提到的 List,還可以生成 Set、Map 等,如下:

// 獲取籃球隊中學生年齡的分布,不允許重復
Set<Integer> ageSet = basketballStudents.stream()
    .map(Student::getAge)
    .collect(Collectors.toSet());
System.out.println("set: " + ageSet);

// 獲取籃球隊中所有學生的姓名和年齡的 Map
Map<String, Integer> nameAndAgeMap = basketballStudents.stream()
    .collect(Collectors.toMap(Student::getName, Student::getAge));
System.out.println("map: " + nameAndAgeMap);

輸出:

set: [17, 18, 19]
map: {Ted=17, Tom=19, Bob=18, Zeka=19}

(2) 生成字符串

除了生成集合,collect還可以用于拼接字符串。

例如,我們獲取籃球隊中所有學生的姓名后,希望用“,”將所有姓名拼接成一個字符串并返回。

System.out.println(basketballStudents.stream()
    .map(Student::getName)
    .collect(Collectors.joining(",")));

輸出:

Bob,Ted,Zeka,Tom

也許你會說,用String.join()不也能實現這個功能嗎?確實,如果只是單純的字符串拼接,確實沒有必要使用Stream來實現。畢竟,殺雞焉用牛刀!

此外,Collectors.joining()還支持定義前綴和后綴,功能更強大。

System.out.println(basketballStudents.stream()
    .map(Student::getName)
    .collect(Collectors.joining(",", "(", ")")));

輸出:

(Bob,Ted,Zeka)

(3) 生成統計結果

還有一個在實際中可能很少用到的場景,就是使用collect生成數字數據的統計結果。我們簡單看一下。

// 計算平均年齡
System.out.println("平均年齡:" + basketballStudents.stream()
    .map(Student::getAge)
    .collect(Collectors.averagingInt(a -> a)));

// 統計匯總
IntSummaryStatistics summary = basketballStudents.stream()
    .map(Student::getAge)
    .collect(Collectors.summarizingInt(a -> a));
System.out.println("summary: " + summary);

在上面的例子中,使用collect對年齡進行了一些數學運算,結果如下:

平均年齡:18.0
summary: IntSummaryStatistics{count=3, sum=54, min=17, average=18.000000, max=19}

四、并行 Stream

使用并行流可以有效利用計算機性能,提高執行速度。并行 Stream 將整個流分成多個片段,然后并行處理每個片段的流,最后將每個片段的執行結果匯總成一個完整的 Stream。

如下圖所示,篩選出大于等于 18 的數字:

將原始任務拆分為多個任務。

[7, 18, 18]

每個任務并行執行操作。

stream.filter(a -> a >= 18)

單個任務處理并匯總為單個結果。

[18, 18]

高效使用 findAny()

如上所述,findAny()在并行 Stream 中更高效,從 API 文檔中可以看出,每次執行該方法的結果可能不同。

使用parallelStream執行findAny()10 次,以找出任何滿足條件(名字是 Bob、Tom 或 Zeka)的學生名字。

for (int i = 0; i < 10; i++) {
    basketballStudents.parallelStream()
        .filter(s -> s.getAge() >= 18)
        .findAny()
        .map(Student::getName)
        .ifPresent(name -> System.out.println("并行流中的 findAny: " + name));
}

輸出:

并行流中的findAny: Zeka
并行流中的findAny: Zeka
并行流中的findAny: Tom
并行流中的findAny: Zeka
并行流中的findAny: Zeka
并行流中的findAny: Bob
并行流中的findAny: Zeka
并行流中的findAny: Zeka
并行流中的findAny: Zeka

這個輸出證實了findAny()的不穩定性。

關于并行流的更多知識,我將在后續文章中進一步分析和討論。

五、注意事項

1. 延遲執行

Stream 是惰性的;只有在啟動終止操作時才會對源數據執行計算,并且只在需要時才會消耗源元素。前面提到的peek方法就是一個很好的例子。

2. 避免執行兩次終止操作

一旦 Stream 被終止,就不能再用于執行其他操作,否則會報錯。看下面的例子:

Stream<Student> stream = students.stream();
stream.filter(s -> s.getAge() >= 18).count();
stream.filter(s -> s.getAge() >= 18).forEach(System.out::println); // 這里會報錯

輸出:

java.lang.IllegalStateException: stream has already been operated upon or closed

因為一旦 Stream 被終止,就不能再重復使用。

責任編輯:趙寧寧 來源: 程序猿技術充電站
相關推薦

2020-10-09 08:15:11

JsBridge

2018-09-26 16:04:04

NVMe主機控制器

2017-09-05 08:52:37

Git程序員命令

2022-02-21 09:44:45

Git開源分布式

2023-05-12 08:19:12

Netty程序框架

2019-04-17 15:16:00

Sparkshuffle算法

2021-04-09 08:40:51

網絡保險網絡安全網絡風險

2021-06-30 00:20:12

Hangfire.NET平臺

2024-06-25 08:18:55

2023-09-06 14:57:46

JavaScript編程語言

2020-12-08 08:09:49

SVG圖標Web

2019-09-24 14:19:12

PythonC語言文章

2021-05-18 08:30:42

JavaScript 前端JavaScript時

2021-06-24 09:05:08

JavaScript日期前端

2021-09-27 09:18:30

ListIterato接口方法

2021-01-26 23:46:32

JavaScript數據結構前端

2021-03-05 18:04:15

JavaScript循環代碼

2021-03-09 14:04:01

JavaScriptCookie數據

2024-04-19 14:23:52

SwitchJavaScript開發

2023-07-28 07:14:13

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品视频999 | 91社区在线观看高清 | 91久久 | 一区免费| 美国av片在线观看 | 日韩一区中文字幕 | 999精品在线 | 国产做a爱免费视频 | 五月免费视频 | 7777在线视频免费播放 | 精品99爱视频在线观看 | av中文字幕在线播放 | 亚洲免费人成在线视频观看 | 亚洲精品永久免费 | 亚洲人成人一区二区在线观看 | 在线观看国产视频 | 欧美日产国产成人免费图片 | 一区二区三区不卡视频 | 久久久久综合 | 免费视频一区 | 涩涩视频在线观看 | 亚洲精品视频在线看 | 日韩精品二区 | 国产精品一区二区久久久久 | 成人免费小视频 | 久草新在线 | 特级做a爱片免费69 精品国产鲁一鲁一区二区张丽 | 欧美区在线 | 国产精品资源在线 | 亚洲欧美中文字幕 | 中文字幕av网 | 天天夜天天操 | 中文字幕国产 | 欧美成人一区二区 | 草久在线视频 | 女朋友的闺蜜3韩国三级 | 黄网站免费在线看 | 精品一区二区观看 | 色播久久久 | 欧美综合在线观看 | 国产高清在线精品一区二区三区 |