Stream 流是 Java8 新增,用于集合 / 数组的函数式编程,简化遍历、筛选、转换、聚合代码,告别繁琐 for 循环,链式调用。
包:java.util.stream.*

一、Stream 流定义与核心概念

1. 什么是 Stream 流

Stream(流)是 数据处理流水线 , 不存储数据 ,数据源来自集合、数组等。它可以对数据源做一系列中间操作,最后通过终端操作得到结果。

类比工厂流水线:原材料(集合)→过滤、加工(中间操作)→产出成品(终端操作)

2. Stream 五大核心特性

  1. 无存储 :Stream 本身不保存元素,元素保存在原始数据源。流只是操作规则。
  2. 惰性求值(延迟执行) : 中间操作不会立刻执行 ,只有调用终端操作时,所有中间操作才一次性执行。
  3. 不修改源数据 :所有操作不会改动原始集合 / 数组,返回新流或新结果。
  4. 一次性消费 :流一旦执行终端操作,流就关闭销毁,不能重复使用,重复使用抛出 IllegalStateException 。
  5. 支持串行 / 并行 : stream() 串行单线程; parallelStream() 自动多线程并行处理大数据。

3. Stream 使用三步固定流程

① 获取流 :拿到数据源对应的 Stream 对象 ② 中间操作 :链式调用,对数据做多次处理(返回新 Stream) ③ 终端操作 :触发流水线执行,生成结果,流关闭

必须:中间操作可以多个,终端操作只能一个,且写在最后

二、Stream 流的获取方式(4 大类)

import java.util.*;
import java.util.stream.Stream;

public class GetStreamDemo {
    public static void main(String[] args) {
        //1. Collection单列集合获取流:集合.stream()
        List list = new ArrayList();
        Stream stream1 = list.stream();
        Stream parallelStream = list.parallelStream(); //并行流

        //2. Map双列集合获取流(Map没有直接stream(),要转单列)
        Map map = new HashMap();
        Stream keyStream = map.keySet().stream(); //键集合流
        Stream valueStream = map.values().stream(); //值集合流
        Stream entryStream = map.entrySet().stream(); //键值对流

        //3. 数组获取流:Arrays.stream(数组)
        String[] arr = {"A","B","C"};
        Stream stream3 = Arrays.stream(arr);

        //4. 多个零散数据直接创建流:Stream.of(T... values)
        Stream stream4 = Stream.of("张三","李四","王五");
    }
}
⚠️注意:Stream.of()底层是数组,不能传 null 数组,会空指针。

三、Stream 操作分类(重点)

分为 中间操作(Intermediate) 和 终端操作(Terminal)

表格

分类特点常用 API返回值
中间操作链式调用、惰性执行,返回新 Streamfilter、map、flatMap、sorted、distinct、limit、skip、peekStream
终端操作触发流水线,执行完毕流关闭,不再返回 StreamforEach、collect、count、max、min、reduce、anyMatch、allMatch、noneMatchvoid / 普通值(Long、Optional、集合等)

一句话区分: 返回 Stream 就是中间操作;返回其他 /void 就是终端操作

四、常用中间操作详解 + 代码示例

1. filter 过滤(Predicate 接口)

作用:根据条件筛选元素,保留返回 true 的元素

List list = Arrays.asList(1,2,3,4,5,6);
//保留大于3的数字
List res = list.stream()
        .filter(num -> num >3)
        .collect(Collectors.toList());
System.out.println(res); // [4,5,6]

2. map 映射转换(Function 接口)

作用:对流中每一个元素做类型 / 内容转换,一个元素映射成一个新元素

List nameList = Arrays.asList("张三","李四","王五");
//把每个名字转为名字长度
List lenList = nameList.stream()
        .map(String::length)
        .collect(Collectors.toList());
System.out.println(lenList); // [2,2,2]

对象场景: map(Movie::getActor) 提取电影对象的演员属性,就是你前面代码用到的!

3. flatMap 扁平化映射

作用:把每个元素转成流,然后把所有流合并成一个大流。用于拆分集合里面嵌套集合。

List nestedList = Arrays.asList(
        Arrays.asList("a","b"),
        Arrays.asList("c","d")
);
//扁平化,把两层集合拆成单层流
List flat = nestedList.stream()
        .flatMap(Collection::stream)
        .collect(Collectors.toList());
System.out.println(flat); // [a,b,c,d]

4. sorted 排序

  • sorted() :自然排序,元素类需要实现 Comparable 接口
  • sorted(Comparator) :自定义比较器排序(推荐)
List nums = Arrays.asList(5,1,3,9,2);
//升序
List sortAsc = nums.stream().sorted().collect(Collectors.toList());
//降序
List sortDesc = nums.stream().sorted((o1,o2)->o2-o1).collect(Collectors.toList());
System.out.println(sortAsc); // [1,2,3,5,9]

5. distinct 去重

底层依靠 equals() 和 hashCode() 方法

List numList = Arrays.asList(1,2,2,3,3,3);
List distinctList = numList.stream()
        .distinct()
        .collect(Collectors.toList());
System.out.println(distinctList); // [1,2,3]

(tips)自定义对象去重

必须重写 equals 和 hashCode

对象去重要在实体类内部重写equals和hashcode方法(Stream底层去重会调用linkHashSet,会先比较哈希值,如果相同则会调用object原生equals默认比较存储地址)

  • hashcode:由地址获取哈希值转变为属性获取
  • equals:直接比较存储地址变为先比较地址,然后保证参数对象不为空且属于实体类,最后返回属性比较的布尔值
public class Student {
    private Integer id;
    private String name;

    // 构造、get、set、toString
    
    // ✅重写equals:按业务属性判断对象内容是否相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        // 业务:id和name相同,就认为是同一个学生
        return Objects.equals(id, student.id) && Objects.equals(name, student.name);
    }

    // ✅重写hashCode:和equals使用完全相同的属性生成哈希码
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

6. limit (long maxSize):截取前 N 个元素

List list = Arrays.asList(10,20,30,40,50);
List limitList = list.stream()
        .limit(3)
        .collect(Collectors.toList());
System.out.println(limitList); // [10,20,30]

7. skip (long n):跳过前 N 个元素

List list = Arrays.asList(10,20,30,40,50);
List skipList = list.stream()
        .skip(2)
        .collect(Collectors.toList());
System.out.println(skipList); // [30,40,50]

limit+skip 组合可以实现分页:skip (页码 - 1 * 每页条数).limit (每页条数)

8. peek 调试用,遍历元素,不修改流

多用于打印看中间数据,开发调试。

List res = Stream.of(1,2,3)
        .peek(num-> System.out.println("原始:"+num))
        .map(n->n*2)
        .collect(Collectors.toList());

五、常用终端操作详解 + 代码示例

1. forEach (Consumer):遍历流中元素

Stream.of("A","B","C").forEach(System.out::println);

❗注意:forEach 是终端操作,执行完流就关闭,不能继续链式调用。

2. collect (Collectors):收集,最常用!

把流的数据收集回集合(List/Set/Map),需要导入 java.util.stream.Collectors

List list = Stream.of("a","b","c").collect(Collectors.toList());
Set set = Stream.of("a","b","c").collect(Collectors.toSet());
// 收集到Map:参数1-key,参数2-value,参数3解决key重复冲突
Map map = Stream.of("张三","李四")
        .collect(Collectors.toMap(s->s, s->s.length(),(oldVal,newVal)->oldVal));
  • Collectors.joining() :字符串拼接
String str = Stream.of("张三","李四","王五").collect(Collectors.joining(","));
System.out.println(str); //张三,李四,王五

3. count ():统计元素个数,返回 long

long count = Stream.of(1,2,3,4).filter(n->n>2).count();
System.out.println(count); //2

4. max () /min ():获取最大 / 最小值,返回 Optional

Optional max = Stream.of(5,2,9,1).max(Integer::compareTo);
Optional min = Stream.of(5,2,9,1).min(Integer::compareTo);
System.out.println(max.get()); //9

5. reduce 归约、聚合,把流元素反复合并成一个结果

求和、求乘积

// 参数1:初始值;参数2:累加器
int sum = Stream.of(1,2,3,4).reduce(0,Integer::sum);
System.out.println(sum); //10

6. 匹配操作(返回 boolean)

  • allMatch() :全部元素满足条件返回 true
  • anyMatch() :任意一个满足返回 true
  • noneMatch() :全部都不满足返回 true
List num = Arrays.asList(2,4,6);
boolean allEven = num.stream().allMatch(n->n%2==0); //true
boolean has5 = num.stream().anyMatch(n->n==5); //false

六、原始类型流(IntStream、LongStream、DoubleStream)

普通 Stream 会有自动装箱拆箱损耗,大数据推荐使用原始流。 mapToInt / mapToLong / mapToDouble 转为原始流

List list = Arrays.asList(1,2,3,4);
int sum = list.stream().mapToInt(Integer::intValue).sum();
System.out.println(sum);

七、综合实战案例(电影对象)

场景:Movie 类,name 电影名,actor 演员名字;筛选出演员包含 star 的电影,收集为 List

class Movie{
    private String name;
    private String actor;
    //构造器 get set toString
    public String getActor() {return actor;}
}

public class MovieStreamDemo {
    public static void main(String[] args) {
        List movies = new ArrayList();
        movies.add(new Movie("电影1","张三"));
        movies.add(new Movie("电影2","李四"));
        movies.add(new Movie("电影3","张三"));
        String star = "张三";

        //Stream写法:筛选演员包含star的电影,收集
        List result = movies.stream()
                .filter(movie -> movie.getActor().contains(star))
                .collect(Collectors.toList());

        // 删除符合条件的电影(就是你之前removeIf底层就是Predicate流思想)
        movies.removeIf(movie -> movie.getActor().contains(star));
    }
}

八、Stream 常见坑点

  1. 流只能使用一次! 终端调用之后流失效,再次使用抛异常
Stream stream = Stream.of("a","b");
stream.forEach(System.out::println);
stream.count(); //报错 IllegalStateException

惰性求值陷阱 :如果只有中间操作,没有终端操作,代码完全不执行!

Stream.of(1,2,3).filter(n->{
    System.out.println(n);
    return n>1;
});
// 控制台无任何输出!没有终端操作,中间代码不运行

distinct 自定义对象必须重写 equals 和 hashCode
并行流 parallelStream() 不要随便用,有线程安全问题,适合纯无状态数据。
map 和 flatMap 容易混淆:map 一对一转换;flatMap 一对多,扁平化拆分流。

九、Stream 对比传统 for 循环

传统 for: 怎么做(How) ,手动控制循环、索引、判断。

Stream: 做什么(What) ,声明式编程,只写业务条件,不用关心循环细节,代码简洁。

Stream流
├─定义:Java8,流水线,不存数据,惰性求值
├─获取流:Collection.stream()|Map转单列|Arrays.stream|Stream.of
├─中间操作(返回Stream)
│  ├─filter过滤、map转换、flatMap扁平化
│  ├─sorted排序、distinct去重、limit截取、skip跳过
├─终端操作(关闭流)
│  ├─collect收集、forEach遍历、count计数
│  ├─max/min、reduce归约、allMatch/anyMatch匹配
└─特性:一次性消费、不修改源数据、串行并行