获取 Stream | JavaSE

获取 Stream

一、学习目标

完成本章后,你应该能够:

  • 能够解释“获取 Stream”到底意味着什么。
  • 能够从 Collection 体系集合获得 Stream。
  • 能够正确处理 Map,分别获取键、值和键值对的 Stream。
  • 能够使用 Arrays.stream() 从对象数组和基本类型数组获得 Stream。
  • 能够使用 Stream.of() 根据若干对象创建 Stream。
  • 能够区分 Stream<T>IntStreamLongStreamDoubleStream
  • 能够避免 Stream.of(int[]) 等基本类型数组场景中的类型误判。
  • 能够根据数据源类型选择最自然的 Stream 获取方式。

二、核心知识

2.1 为什么必须先“获取 Stream”

上一章我们建立了 Stream Pipeline:

数据源 Source
     ↓
中间操作
     ↓
中间操作
     ↓
终结操作

那么第一步一定是:

让原始数据进入 Stream 流水线。

例如我们原本有:

List<String> names =
        List.of(
                "张无忌",
                "赵敏",
                "周芷若"
        );

它本身是:

List<String>

如果想使用:

filter()
map()
sorted()
limit()

等 Stream API,需要先:

names.stream()

得到:

Stream<String>

于是:

List<String>
     ↓
stream()
     ↓
Stream<String>
     ↓
开始函数式数据处理

所以“获取 Stream”并不是:

把所有数据复制进一个新的 Stream 集合。

更准确的理解是:

基于现有数据源建立一条 Stream 数据处理流水线。


2.2 常见数据源

原课程主要要求掌握:

Collection
Map
数组
若干独立数据

对应的主流入口:

Collection
    ↓
collection.stream()

Map
    ↓
keySet() / values() / entrySet()
    ↓
stream()

对象数组
    ↓
Arrays.stream(array)

若干对象
    ↓
Stream.of(...)

这是本章主线。


三、从 Collection 获取 Stream

3.1 Collection 的 stream()

Collection<E> 提供:

Stream<E> stream()

因此所有常见的 Collection 子体系:

  • List
  • Set
  • Queue

都可以直接:

集合对象.stream()

3.2 List 获取 Stream

例如:

import java.util.List;
import java.util.stream.Stream;

public class StreamDemo01 {

    public static void main(String[] args) {

        List<String> names =
                List.of(
                        "张无忌",
                        "赵敏",
                        "周芷若"
                );

        Stream<String> stream =
                names.stream();
    }
}

类型关系:

List<String>
     ↓
stream()
     ↓
Stream<String>

Stream 中元素类型仍然是:

String

3.3 Set 获取 Stream

同理:

Set<String> names =
        Set.of(
                "Java",
                "MySQL",
                "Spring"
        );

Stream<String> stream =
        names.stream();

因为:

Set<E>

也是:

Collection<E>

3.4 为什么 List 和 Set 都能直接 stream()

因为:

        Collection<E>
          /       \
         /         \
      List<E>     Set<E>

stream() 定义在:

Collection

体系中。

所以:

ArrayList
LinkedList
HashSet
LinkedHashSet
TreeSet

都可以通过继承到的 stream() 获得 Stream。


四、从 Map 获取 Stream

4.1 为什么 Map 不能直接 stream()

这是非常重要的区别。

我们前面学习集合体系时已经知道:

Collection
和
Map

是两套不同的顶层体系。

Map

Map<K, V>

保存的是:

key → value

键值映射。

而:

stream()

Collection 接口提供的能力。

因此:

Map<String, Integer> map =
        new HashMap<>();

不能直接写:

// 错误
// map.stream();

4.2 Map 的解决办法

Map 前面已经学习过三种集合视图:

map.keySet()
map.values()
map.entrySet()

它们分别得到:

Set<K>

Collection<V>

Set<Map.Entry<K,V>>

这些都是 Collection 体系。

所以可以继续:

Map
 ↓
转换成 Collection 视图
 ↓
stream()

4.3 获取键的 Stream

例如:

Map<String, Integer> map =
        new HashMap<>();

map.put("Java", 100);
map.put("MySQL", 90);
map.put("Spring", 95);

获取键:

Stream<String> keyStream =
        map.keySet().stream();

过程:

Map<String, Integer>
        ↓
keySet()
        ↓
Set<String>
        ↓
stream()
        ↓
Stream<String>

4.4 获取值的 Stream

Stream<Integer> valueStream =
        map.values().stream();

过程:

Map<String, Integer>
        ↓
values()
        ↓
Collection<Integer>
        ↓
stream()
        ↓
Stream<Integer>

4.5 获取键值对 Stream

这是实际开发中非常常用的形式:

Stream<Map.Entry<String, Integer>>
        entryStream =
        map.entrySet().stream();

其中每一个元素都是:

Map.Entry<String, Integer>

也就是:

一个 key-value 键值对对象

因此以后可以处理:

entry.getKey()
entry.getValue()

4.6 Map 三种 Stream 入口总结

Map<String, Integer> map =
        new HashMap<>();

可以:

Stream<String> keys =
        map.keySet().stream();

Stream<Integer> values =
        map.values().stream();

Stream<Map.Entry<String, Integer>> entries =
        map.entrySet().stream();

选择哪一种取决于:

你的业务到底需要处理 key、value,还是完整键值对。


五、从数组获取 Stream

5.1 对象数组:Arrays.stream()

假设:

String[] names = {
        "张三丰",
        "张无忌",
        "张翠山"
};

可以:

Stream<String> stream =
        Arrays.stream(names);

完整代码:

import java.util.Arrays;
import java.util.stream.Stream;

public class StreamDemo02 {

    public static void main(String[] args) {

        String[] names = {
                "张三丰",
                "张无忌",
                "张翠山"
        };

        Stream<String> stream =
                Arrays.stream(names);
    }
}

5.2 Arrays.stream() 的区间形式

Java 还支持:

Arrays.stream(
        array,
        startInclusive,
        endExclusive
)

例如:

String[] names = {
        "A",
        "B",
        "C",
        "D"
};

Stream<String> stream =
        Arrays.stream(
                names,
                1,
                3
        );

范围遵循 Java 常见的:

[startInclusive, endExclusive)

即:

[1, 3)

包含:

下标 1
下标 2

不包含下标 3


六、基本类型数组与专用 Stream

6.1 int[] 会得到 IntStream

假设:

int[] numbers = {
        10,
        20,
        30
};

使用:

Arrays.stream(numbers)

返回的并不是:

Stream<Integer>

而是:

IntStream

因此:

IntStream stream =
        Arrays.stream(numbers);

需要导入:

import java.util.stream.IntStream;

6.2 为什么 Java 有 IntStream

Java 泛型不能直接使用基本类型:

Stream<int>

这是非法的。

如果全部包装为:

Stream<Integer>

大量数值处理可能产生:

int
↕
Integer

装箱和拆箱。

因此 Stream API 为常见基本数值类型提供:

IntStream
LongStream
DoubleStream

例如:

int[] → IntStream

long[] → LongStream

double[] → DoubleStream

6.3 对象数组与基本类型数组对比

对象数组:

String[] names = {
        "Java",
        "MySQL"
};

Stream<String> stream =
        Arrays.stream(names);

基本类型数组:

int[] numbers = {
        1,
        2,
        3
};

IntStream stream =
        Arrays.stream(numbers);

要形成清晰认知:

T[]
 ↓
Stream<T>


int[]
 ↓
IntStream


long[]
 ↓
LongStream


double[]
 ↓
DoubleStream

七、Stream.of()

7.1 根据若干对象创建 Stream

除了从集合、数组获得 Stream,还可以直接:

Stream.of(...)

例如:

Stream<String> stream =
        Stream.of(
                "Java",
                "MySQL",
                "Spring"
        );

得到:

Java
MySQL
Spring

三个 Stream 元素。


7.2 单个元素

也可以:

Stream<String> stream =
        Stream.of("Java");

得到只包含一个元素的 Stream。


7.3 对象数组

原课程还给出了:

String[] names = {
        "张三丰",
        "张无忌",
        "张翠山"
};

Stream<String> stream =
        Stream.of(names);

这里 Stream.of(T...) 使用可变参数。

对于:

String[]

这样的引用类型数组,可以得到对应:

Stream<String>

因此对象数组场景下:

Arrays.stream(names)

与:

Stream.of(names)

都可以完成常见需求。

初学阶段,对已有数组更推荐使用语义直接的:

Arrays.stream(names)

八、一个非常重要的坑:Stream.of(int[])

8.1 错误直觉

假设:

int[] numbers = {
        1,
        2,
        3
};

很多初学者会认为:

Stream.of(numbers)

能够得到:

1
2
3

但这不是正确理解。


8.2 实际类型

因为:

int[]

本身是一个对象类型。

因此:

Stream.of(numbers)

会把整个:

int[]

数组当成一个元素

类型类似:

Stream<int[]>

而不是:

IntStream

更不是:

Stream<Integer>

8.3 正确做法

如果要逐个处理:

1
2
3

应该:

IntStream stream =
        Arrays.stream(numbers);

所以必须记住:

String[]
    ↓
Stream.of(array)
通常可展开为对象元素


int[]
    ↓
Stream.of(array)
整个 int[] 可能成为一个元素


int[] 正确数值流入口
    ↓
Arrays.stream(array)
    ↓
IntStream

这是 Stream 获取阶段非常典型的类型陷阱。


九、stream() 与 parallelStream()

Collection 还提供:

parallelStream()

例如:

Stream<String> stream =
        names.parallelStream();

它用于获得可能并行执行的 Stream。

但是当前 JavaSE 主线首先学习:

stream()

顺序流。

原因是:

并行 Stream 并不等于“自动更快”。

并行执行还涉及:

  • 数据规模
  • 操作成本
  • 线程调度
  • 顺序约束
  • 共享状态
  • Fork/Join

等问题。

所以本阶段不要养成:

“看到 Stream 就用 parallelStream()”

的习惯。

先把顺序 Stream 的语义掌握清楚。


十、不同数据源的统一模型

到这里,可以把 Stream 获取整理成:

                 数据源

      ┌────────────┼─────────────┐
      │            │             │
 Collection       Map           数组
      │            │             │
 stream()      三种视图      Arrays.stream()
                   │
                   │
          keySet / values
             / entrySet
                   │
                stream()

另外:
Stream.of(...)
    ↓
直接根据若干对象
建立 Stream

真正需要掌握的是:

看到数据源以后,立即知道应该从哪里进入 Stream Pipeline。


十一、实践应用

11.1 List

List<Student> students =
        new ArrayList<>();

Stream<Student> stream =
        students.stream();

以后可以:

Student
   ↓
过滤
排序
映射
统计

11.2 Set

Set<String> tags =
        new HashSet<>();

Stream<String> stream =
        tags.stream();

11.3 Map

假设星雨笔录需要统计:

文章分类 → 文章数量

数据:

Map<String, Integer>
        articleCount;

如果需要同时查看分类和数量:

articleCount
        .entrySet()
        .stream();

通常比单独:

keySet()

更合适。


11.4 数组

算法程序中可能存在:

int[] scores;

可以:

Arrays.stream(scores)

得到:

IntStream

然后进一步进行数值处理。


十二、常见问题

12.1 Stream 是从数据源复制出一份数据吗?

不要这样理解。

获取 Stream 的核心是:

基于数据源建立数据处理流水线。


12.2 List 和 Set 为什么都能 stream()?

因为它们属于:

Collection

体系。


12.3 Map 为什么不能直接 stream()?

因为:

Map

不是 Collection 子接口。

要先通过:

keySet()
values()
entrySet()

获得 Collection 视图。


12.4 Map 最推荐哪一种?

没有绝对答案。

只处理键:

map.keySet().stream()

只处理值:

map.values().stream()

同时需要键和值:

map.entrySet().stream()

12.5 数组一定得到 Stream 吗?

不是。

例如:

int[]

通过:

Arrays.stream()

得到:

IntStream

12.6 Stream.of(int[]) 有什么问题?

它可能产生:

Stream<int[]>

即整个数组成为一个 Stream 元素。

逐个处理整数应该使用:

Arrays.stream(intArray)

12.7 stream() 得到以后可以一直重复使用吗?

不能这样设计。

一个 Stream Pipeline 通常只能消费一次。

如果需要重新处理:

list.stream()

重新建立一条 Stream。


十三、练习与验收

13.1 知识问答

  1. 为什么使用 Stream API 前通常需要先“获取 Stream”?
  2. Collection.stream() 返回什么?
  3. 为什么 List 和 Set 都能直接调用 stream()
  4. 为什么 Map 不能直接调用 stream()
  5. Map 有哪三种常见 Stream 获取方案?
  6. entrySet().stream() 中每个元素是什么类型?
  7. 对象数组怎样获得 Stream?
  8. int[] 使用 Arrays.stream() 得到什么?
  9. Stream<String>IntStream 有什么区别?
  10. Stream.of() 解决什么问题?
  11. Stream.of(int[]) 为什么容易产生误解?
  12. stream()parallelStream() 有什么基本区别?

13.2 代码阅读

判断类型:

List<String> list =
        List.of("A", "B");

var stream =
        list.stream();

stream 的实际泛型类型是什么?


分析:

Map<String, Integer> map =
        new HashMap<>();

var stream =
        map.entrySet().stream();

Stream 中每个元素是什么?


分析:

int[] numbers = {
        1,
        2,
        3
};

var a = Arrays.stream(numbers);
var b = Stream.of(numbers);

回答:

  1. a 属于什么类型?
  2. b 中有几个顶层 Stream 元素?
  3. 两种写法的语义为什么不同?

13.3 手写代码

不查看资料分别完成:

  1. ArrayList<String> 获取 Stream。
  2. HashSet<Student> 获取 Stream。
  3. Map<String,Integer> 获取 key Stream。
  4. 获取 value Stream。
  5. 获取 Entry Stream。
  6. String[] 获取 Stream。
  7. int[] 获取数值 Stream。
  8. 使用 Stream.of() 创建包含 "Java"、"MySQL"、"Spring" 的 Stream。

13.4 Debug

下面代码有什么问题?

Map<String, Integer> map =
        new HashMap<>();

// map.stream();

说明原因并给出三种正确思路。


分析:

int[] numbers = {
        1,
        2,
        3
};

Stream<int[]> stream =
        Stream.of(numbers);

代码本身可以表达什么语义?

如果需求是:

分别处理 1、2、3

应该如何调整?

13.5 综合训练

给出:

List<Student> students;

Map<String, Integer> scoreMap;

String[] names;

int[] scores;

分别写出最合适的 Stream 获取方式,并说明:

原数据类型
→
Stream 类型
→
Stream 中单个元素的类型

13.6 本章验收

闭卷画出:

Collection
  ↓
stream()

Map
  ↓
keySet / values / entrySet
  ↓
stream()

T[]
  ↓
Arrays.stream()
  ↓
Stream<T>

int[]
  ↓
Arrays.stream()
  ↓
IntStream

若干对象
  ↓
Stream.of(...)

如果能够根据数据源类型立即选择正确 Stream 入口,并能够解释:

Arrays.stream(intArray)

与:

Stream.of(intArray)

的根本区别,则本章达到掌握标准。