什么是Stream

Stream 可以理解成一条数据处理流水线。
它不是直接去操作集合本身,而是:先把集合变成流,再对流里的元素做一系列处理,最后得到结果

核心思路:原始数据 -> 加工处理 -> 得到结果

1
2
3
集合.stream()
.一系列操作
.最终得到结果

常见处理流程

最常见的写法:

1
2
3
4
5
6
7
List<String> result = list.stream() // 先从集合开始
.filter(Objects::nonNull) // 【过滤数据】去掉`null`
.map(String::valueOf) // 【转换数据】转成字符串
.distinct() // 去重
.sorted() // 排序
.limit(10) // 取前 10 个
.collect(Collectors.toList()); // 收集成新集合


函数介绍

Stream核心流程:流进来 -> 处理 -> 收集结果

streamfilter筛选
map转换
forEach执行动作
sorted排序
distinct去重
skip跳过
limit截取
count计数
findFirst找第一个
collect收集
toList收集成列表
toSet收集成去重集合
toMap收集成键值对
groupingBy按字段分组
counting()每组数量
mapping()每组提取字段
summingInt()每组求和

Stream 执行顺序

这点很重要。
不是先把所有元素都 map,再把所有元素都 filter。而是一个元素一个元素地往下走。

1
2
3
4
list.stream()
.filter(...)
.map(...)
.collect(...)

执行方式更像:

  1. 第一个元素先过 filter
  2. 如果通过,再过 map
  3. 然后处理下一个元素
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
    List<String> result = numbers.stream()
    .filter(n -> {
    System.out.println("filter: " + n);
    return n % 2 == 0;
    })
    .map(n -> {
    System.out.println("map: " + n);
    return "数字" + n;
    })
    .collect(Collectors.toList());
    输出顺序会更像:
    1
    2
    3
    4
    5
    6
    filter: 1
    filter: 2
    map: 2
    filter: 3
    filter: 4
    map: 4

mapfilterforEach

分别是干什么的

  • map 的作用是:把一个元素转换成另一个元素。一句话理解:一个元素进来,一个新元素出去
    1
    2
    3
    4
    // 这里是把数字转换成字符串。
    List<String> list = numbers.stream()
    .map(String::valueOf)
    .collect(Collectors.toList());
  • filter 的作用是:判断这个元素要不要保留。一句话理解:留还是不留
    1
    2
    3
    4
    5
    6
    // 这里表示:
    // - 偶数保留
    // - 奇数过滤掉
    List<Integer> list = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
  • forEach 的作用是:对每个元素执行一个动作。一句话理解:拿每个元素做点事
    1
    2
    // 这里就是把每个元素打印出来。
    numbers.stream().forEach(System.out::println);

    到底返回什么

  • map:返回新元素,转换后的新值
    1
    2
    3
    4
    // 这里返回的是 `String`
    .map(n -> n.toString())
    // 这里返回的是用户名
    .map(user -> user.getName())
  • filter:返回布尔值,truefalse
    1
    2
    // `true`:保留,`false`:过滤掉
    .filter(n -> n > 10)
  • forEach 不需要返回值。
    1
    2
    // 这里只是做动作,不返回结果。
    .forEach(n -> System.out.println(n))

lambda 加 {} 后是不是就必须 return

不完全是。更准确地说,如果这个 lambda 本来就需要返回值,那么加了 {} 后就要自己写 return

  • map{} 需要 returnmap 需要返回新值。
    1
    2
    3
    .map(o -> {
    return o.toString();
    })
  • filter{} 需要 returnfilter 需要返回布尔值。
    1
    2
    3
    .filter(o -> {
    return o != null;
    })
  • forEach{} 不需要 returnforEach 本来就不需要返回值。
    1
    2
    3
    .forEach(o -> {
    System.out.println(o);
    })

String::valueOfo -> o.toString() 区别

这两个通常都能把对象转成字符串。

1
2
3
4
// 写法一
.map(String::valueOf)
// 写法二
.map(o -> o.toString())

最大区别:null

  • o -> o.toString(),如果 onull,会报空指针异常。
  • String::valueOf,如果是 null,不会报错,会返回字符串 "null"
    更安全的写法
    1
    2
    3
    4
    5
    6
    7
    //方案 1:先判断 null
    .map(o -> o == null ? null : o.toString())
    // 方案 2:给 null 默认值
    .map(o -> o == null ? "" : o.toString())
    // 方案 3:先过滤 null
    .filter(Objects::nonNull)
    .map(Object::toString)

为什么一般建议先 filtermap

因为先过滤掉不需要的数据,后面的转换就会少做很多事。

1
2
3
4
5
// 这里先保留偶数,再转字符串,更合理。
numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> "数字" + n)
.collect(Collectors.toList());

好处:

  1. 性能更好,因为 map 只处理留下来的元素。
  2. 逻辑更清晰,更符合“先筛选,再加工”的思路。
  3. 更安全,比如先过滤 null,再 toString(),就不会空指针。

如果过滤条件依赖转换后的结果,就只能先 map

1
2
3
4
5
// 必须先变成字符串,才能判断字符串长度。
numbers.stream()
.map(String::valueOf)
.filter(s -> s.length() == 1)
.collect(Collectors.toList());


sorteddistinctlimit 放前放后有什么区别

顺序不同,结果可能不同。

  • sorted().limit(3)limit(3).sorted() 不一样
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 先排序再取前3个(取最小的3个)
    list.stream()
    .sorted()
    .limit(3)
    .collect(Collectors.toList());
    // 先取前3个再排序(原始前3个元素里排个序)
    list.stream()
    .limit(3)
    .sorted()
    .collect(Collectors.toList());
  • distinct().limit(3)limit(3).distinct() 不一样
  • distinct().sorted()sorted().distinct()
    最终结果有时一样,但一般先去重后排序,先去重后,排序的数据量可能更少
    1
    .distinct().sorted()

skip()limit() 使用

最常见就是分页。

  • skip(n):跳过前 n 个元素。
  • limit(n):取前 n 个元素。
1
2
// 跳过前 `a` 个,再取接下来的 `b` 个
.skip(a).limit(b)

分页公式

1
2
.skip((pageNum - 1) * pageSize)
.limit(pageSize)

1
2
3
4
5
6
7
8
// 跳过前3个,再取4个
List<Integer> result = list.stream()
.skip(3)
.limit(4)
.collect(Collectors.toList());

// 第3页,每页10条:
.skip(10).limit(10)


forEachlist.forEachlist.stream().forEach 区别

  • list.forEach(...),集合本身的方法。直接遍历这个集合中的每个元素,它不经过 Stream 流水线
    1
    list.forEach(System.out::println);
  • list.stream().forEach(...) 先把集合转成 Stream,再调用 Stream 的终止操作。也是遍历输出,但它是 Stream 的写法
    1
    list.stream().forEach(System.out::println);
    它们什么时候看起来一样,如果只是简单遍历打印,效果通常一样
    1
    2
    3
    list.forEach(System.out::println);

    list.stream().forEach(System.out::println);
    真正区别
  • list.forEach(...),直接遍历
    单纯遍历,不需要中间操作,
    1
    list.forEach(System.out::println);
  • list.stream().forEach(...),先走 Stream 流水线,最后再遍历处理
    前面还要 filter,还要 map,还要 sorted
    1
    2
    3
    4
    list.stream()
    .filter(Objects::nonNull)
    .map(String::valueOf)
    .forEach(System.out::println);
    怎么选
    1
    2
    3
    4
    5
    6
    7
    // 只想遍历集合
    list.forEach(...)
    // 想先处理再遍历
    ```java
    list.stream()
    .xxx()
    .forEach(...)

终止操作:count()findFirst()anyMatch()

  • count(),统计数量
    适合场景:有多少条;足条件的有多少个
    1
    2
    3
    long count = numbers.stream()
    .filter(n -> n > 2)
    .count();
  • findFirst(),找到第一个符合条件的元素。返回的是 Optional<T>,常配合 orElse(...) 使用。
    适合场景:找第一个符合条件的元素,找到一个就够了
    1
    2
    3
    4
    Integer value = numbers.stream()
    .filter(n -> n > 2)
    .findFirst()
    .orElse(null);
  • anyMatch(),是否存在任意一个满足条件
    适合场景:有没有空值,有没有异常数据,有没有符合条件的元素
    1
    2
    boolean has = numbers.stream()
    .anyMatch(n -> n > 10);
  • allMatch(),是否全部满足条件
    1
    2
    boolean all = numbers.stream()
    .allMatch(n -> n > 0);
  • noneMatch(),是否一个都不满足
    1
    2
    boolean none = numbers.stream()
    .noneMatch(n -> n < 0);
    记忆方式,这些方法会真正触发 Stream 执行。
  • count():几个
  • findFirst():第一个是谁
  • anyMatch():有没有
  • allMatch():是不是全部
  • noneMatch():是不是一个都没有

collect(...) 是做什么的

collect(...):把 Stream 处理后的结果,收集成你想要的容器
最常见的有:
toList()toSet()toMap()

  • Collectors.toList()
    作用:收集成 List
    特点:保留顺序,允许重复
    1
    List<String> list = stream.collect(Collectors.toList());
  • Collectors.toSet()
    作用:收集成 Set
    特点:自动去重,不要依赖顺序
    1
    Set<String> set = stream.collect(Collectors.toSet());
  • Collectors.toMap()
    作用:收集成 Map
    表示:key:id,value:name
    1
    2
    3
    4
    5
    Map<Integer, String> map = users.stream()
    .collect(Collectors.toMap(
    User::getId,
    User::getName
    ));

    注意:toMap() 默认key不能重复。如果重复会报错。

    如何解决?
    1
    2
    3
    4
    5
    6
    Map<Integer, String> map = list.stream()
    .collect(Collectors.toMap(
    keyMapper,
    valueMapper,
    (oldValue, newValue) -> oldValue // 表示 key 冲突时保留谁
    ));

toMapgroupingBy 区别

  • toMap:一个 key 对一个 value。给每个 key 找一个值。适合“建立索引”。
    1
    2
    3
    4
    5
    6
    // id -> user
    Map<Integer, User> userMap = users.stream()
    .collect(Collectors.toMap(
    User::getId,
    user -> user
    ));
  • groupingBy:一个 key 对一组 value。把同类数据装进同一个桶里。适合“分类分组”。
    1
    2
    3
    // department -> users
    Map<String, List<User>> deptMap = users.stream()
    .collect(Collectors.groupingBy(User::getDept));

groupingBy 和 downstream collector 配合

1
2
3
4
5
[
{name="张三", dept="技术部", salary=10000},
{name="李四", dept="技术部", salary=15000},
{name="王五", dept="销售部", salary=8000}
]
  • groupingBy:按什么分组
  • downstream collector:每组最后变成什么

    groupingBy

    1
    Collectors.groupingBy(分组规则, 每组怎么处理)
  • 普通 groupingBy
    1
    2
    3
    // 部门 -> 用户列表
    Map<String, List<User>> map = users.stream()
    .collect(Collectors.groupingBy(User::getDept));

    groupingBy + counting()

    作用:分组后统计每组数量
    1
    2
    3
    4
    5
    6
    // 部门 -> 人数
    Map<String, Long> map = users.stream()
    .collect(Collectors.groupingBy(
    User::getDept,
    Collectors.counting()
    ));
    1
    2
    3
    4
    {
    "技术部" = 2,
    "销售部" = 1
    }
  • groupingBy + counting()
    1
    2
    3
    4
    5
    6
    // 部门 -> 人数
    Map<String, Long> map = users.stream()
    .collect(Collectors.groupingBy(
    User::getDept,
    Collectors.counting()
    ));

    groupingBy + mapping()

    作用:分组后,不保留整个对象,而是提取某个字段
    mapping:在每个分组内部,再做一次 map
    1
    2
    3
    4
    5
    6
    // 部门 -> 名字列表
    Map<String, List<String>> map = users.stream()
    .collect(Collectors.groupingBy(
    User::getDept,
    Collectors.mapping(User::getName, Collectors.toList())
    ));
    1
    2
    3
    4
    {
    "技术部" = ["张三", "李四"],
    "销售部" = ["王五"]
    }
  • groupingBy + mapping()
    1
    2
    3
    4
    5
    6
    // 部门 -> 姓名列表
    Map<String, List<String>> map = users.stream()
    .collect(Collectors.groupingBy(
    User::getDept,
    Collectors.mapping(User::getName, Collectors.toList())
    ));

    groupingBy + summingInt / summingLong / summingDouble

    作用:分组后对某个数值字段求和
    1
    2
    3
    4
    5
    6
    // 部门 -> 工资总和
    Map<String, Integer> map = users.stream()
    .collect(Collectors.groupingBy(
    User::getDept,
    Collectors.summingInt(User::getSalary)
    ));
  • groupingBy + summingInt()
    1
    2
    3
    4
    5
    6
    // 部门 -> 工资总和
    Map<String, Integer> map = users.stream()
    .collect(Collectors.groupingBy(
    User::getDept,
    Collectors.summingInt(User::getSalary)
    ));

判断方法

你写 Stream 时,可以先问自己下面几个问题。

第一步:我要不要筛选数据?

1
filter(...)

第二步:我要不要把元素转换成别的东西?
1
map(...)

第三步:我要不要对每个元素执行动作?
1
forEach(...)

第四步:最后我要什么结果?

目的 函数
List collect(toList())
Set collect(toSet())
Map collect(toMap())
要数量 count()
要第一个 findFirst()
要真假判断 anyMatch() / allMatch() / noneMatch()