Java Stream流详解:从入门到精通

引言:为什么需要Stream流

想象一下,你需要从一堆水果中挑选出所有红色的、重量超过200克的苹果,并按重量排序取前三个。传统方式需要你写多个循环和条件判断,而使用Stream流,你可以这样做:

List result = apples.stream()
                          .filter(a -> a.getColor().equals("红色"))
                          .filter(a -> a.getWeight() > 200)
                          .sorted((a1, a2) -> a1.getWeight() - a2.getWeight())
                          .limit(3)
                          .collect(Collectors.toList());

Stream流就像一条传送带,数据放上去后可以经过多个处理站,最终得到你想要的结果,让代码更简洁、可读性更强。

一、Stream流基础

1.1 什么是Stream流

Stream流是Java 8引入的处理集合数据的API,它让我们以声明式(描述要做什么,而不是怎么做)的方式处理数据。

核心特点

  • 声明式编程:更关注做什么,而不是怎么做
  • 支持链式操作:可以像搭积木一样组合多个操作
  • 支持并行处理:轻松切换到并行模式,提高性能
  • 延迟执行:在最后一步(终端操作)才会真正执行

1.2 Stream流与集合的区别

简单对比:

  • 集合就像一个仓库,专注于存储和访问数据
  • Stream就像一条流水线,专注于对数据进行操作
特性 集合 Collection Stream流
存储方式 存储元素 不存储元素
操作时机 立即执行 延迟执行(惰性)
遍历次数 可多次遍历 只能遍历一次
关注点 数据 计算

1.3 Stream流的使用步骤

Stream流的使用分为三个步骤:

  1. 创建Stream流:获取一个数据流
  2. 使用中间方法:对流中的数据进行操作
  3. 使用终结方法:对流水线上的数据进行最终操作
// 一个完整的Stream操作示例
List names = Arrays.asList("小明", "小红", "小刚", "小华");

List filteredNames = names.stream()           // 1.创建Stream流
                                 .filter(name -> name.length() > 1)  // 2.中间操作
                                 .collect(Collectors.toList());       // 3.终端操作

二、创建Stream流

2.1 从不同数据源创建Stream

我们可以从多种数据源创建Stream:

获取方式 方法名 说明
单列集合 default Stream stream() Collection中的默认方法
双列集合 无法直接使用stream流
数组 public static Stream stream(T[] array) Arrays工具类中的静态方法
一堆零散数据 public static Stream of(T... values) Stream接口中的静态方法
2.1.1 从单列集合创建Stream(最常用)
// 1.单列集合获取Stream流
ArrayList list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");
// 获取一条流水线,并把集合中的数据放到流水线上
Stream stream1 = list.stream();
// 使用终结方法打印一下流水线上的所有数据
stream1.forEach(new Consumer() {
    @Override
    public void accept(String s) {
        // s:依次表示流水线上的每一个数据
        System.out.println(s);
    }
});
// 使用Lambda简化
list.stream().forEach(s -> System.out.println(s));
2.1.2 从双列集合创建Stream

双列集合没有直接获取Stream流的方法,但可以间接获取:

// 1.创建双列集合
HashMap hm = new HashMap<>();
// 2.添加数据
hm.put("红楼梦", 111);
hm.put("西游记", 222);
hm.put("水浒传", 333);
hm.put("三国演义", 444);

// 3.第一种获取stream流的方式:通过键
hm.keySet().stream().forEach(s -> System.out.println(s));

// 4.第二种获取stream流的方式:通过值
hm.values().stream().forEach(i -> System.out.println(i));

// 5.第三种获取stream流的方式:通过键值对
hm.entrySet().stream().forEach(entry -> System.out.println(entry.getKey() + "=" + entry.getValue()));
2.1.3 从数组创建Stream
// 1.创建数组
int[] arr1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
String[] arr2 = {"a", "b", "c"};

// 2.获取stream流
Arrays.stream(arr1).forEach(s -> System.out.println(s));
System.out.println("========================");
Arrays.stream(arr2).forEach(s -> System.out.println(s));
2.1.4 从零散数据创建Stream
// 一堆零散数据    public static Stream of(T... values)    Stream接口中的静态方法
Stream.of(1, 2, 3, 4, 5).forEach(s -> System.out.println(s));
Stream.of("a", "b", "c", "d", "e").forEach(s -> System.out.println(s));

三、Stream流的中间操作

中间操作会返回一个新的Stream,可以链式调用。中间操作不会立即执行,而是等到终端操作时才一起执行。

Stream的中间方法包括:

名称 说明
filter(Predicate predicate) 过滤
limit(long maxSize) 获取前几个元素
skip(long n) 跳过前几个元素
distinct() 元素去重(依赖hashCode和equals方法)
concat(Stream a, Stream b) 合并a和b两个流为一个流
map(Function mapper) 转换流中的数据类型

3.1 筛选和切片操作

List numbers = Arrays.asList(1, 2, 2, 3, 4, 5);

// filter: 过滤元素
List greaterThan3 = numbers.stream()
                                  .filter(n -> n > 3)
                                  .collect(Collectors.toList());
System.out.println("大于3的数: " + greaterThan3);  // [4, 5]

// distinct: 去除重复
List uniqueNumbers = numbers.stream()
                                   .distinct()
                                   .collect(Collectors.toList());
System.out.println("去重后: " + uniqueNumbers);  // [1, 2, 3, 4, 5]

// limit: 限制数量
List firstThree = numbers.stream()
                                .limit(3)
                                .collect(Collectors.toList());
System.out.println("前三个数: " + firstThree);  // [1, 2, 2]

// skip: 跳过元素
List skipFirst2 = numbers.stream()
                                .skip(2)
                                .collect(Collectors.toList());
System.out.println("跳过前两个: " + skipFirst2);  // [2, 3, 4, 5]

// 组合使用
List result = numbers.stream()
                            .distinct() // 先去重
                            .skip(1)    // 跳过第一个
                            .limit(3)   // 取3个
                            .collect(Collectors.toList());
System.out.println("去重后跳过第一个,再取三个: " + result); // [2, 3, 4]

3.2 映射操作

List words = Arrays.asList("Java", "Stream", "API");

// map: 转换元素
List wordLengths = words.stream()
                               .map(word -> word.length())  // 获取每个单词的长度
                               .collect(Collectors.toList());
System.out.println("单词长度: " + wordLengths);  // [4, 6, 3]

// map的另一个示例:转换大写
List upperCaseWords = words.stream()
                                 .map(word -> word.toUpperCase())
                                 .collect(Collectors.toList());
System.out.println("转换为大写: " + upperCaseWords);  // [JAVA, STREAM, API]

// flatMap: 扁平化处理(将多个流合并为一个流)
List> nestedList = Arrays.asList(
    Arrays.asList(1, 2), 
    Arrays.asList(3, 4, 5)
);
List flatList = nestedList.stream()
                                 .flatMap(list -> list.stream())  // 将每个内部List转为Stream然后合并
                                 .collect(Collectors.toList());
System.out.println("扁平化后: " + flatList);  // [1, 2, 3, 4, 5]

// flatMap实际应用:提取所有单词的字符
List words2 = Arrays.asList("Hello", "World");
List letters = words2.stream()
                           .flatMap(word -> Arrays.stream(word.split("")))
                           .collect(Collectors.toList());
System.out.println("所有字符: " + letters);
// [H, e, l, l, o, W, o, r, l, d]

3.3 排序操作

List names = Arrays.asList("小明", "小红", "小刚", "小华");

// 自然排序
List sortedNames = names.stream()
                              .sorted()
                              .collect(Collectors.toList());
System.out.println("自然排序: " + sortedNames);  

// 自定义排序(按字符串长度)
List sortedByLength = names.stream()
                                 .sorted((s1, s2) -> s1.length() - s2.length())
                                 .collect(Collectors.toList());
System.out.println("按长度排序: " + sortedByLength);  

// 逆序排序
List numbers = Arrays.asList(5, 3, 8, 1, 4);
List reverseSorted = numbers.stream()
                                   .sorted((n1, n2) -> n2 - n1)
                                   .collect(Collectors.toList());
System.out.println("逆序排序: " + reverseSorted);  // [8, 5, 4, 3, 1]

// 多条件排序
class Person {
    String name;
    int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() { return name; }
    public int getAge() { return age; }
    @Override
    public String toString() { return name + ":" + age; }
}

List people = Arrays.asList(
    new Person("小明", 20),
    new Person("小红", 25),
    new Person("小刚", 20)
);

// 先按年龄排序,年龄相同按姓名排序
List sortedPeople = people.stream()
    .sorted((p1, p2) -> {
        // 先比较年龄
        int result = p1.getAge() - p2.getAge();
        // 如果年龄相同,再比较姓名
        if (result == 0) {
            return p1.getName().compareTo(p2.getName());
        }
        return result;
    })
    .collect(Collectors.toList());
System.out.println("多条件排序: " + sortedPeople);
// [小刚:20, 小明:20, 小红:25]

3.4 查看操作(peek)

// peek: 用于调试,查看中间结果
List result = Stream.of("one", "two", "three")
                         .peek(e -> System.out.println("原始元素: " + e))
                         .map(word -> word.toUpperCase())
                         .peek(e -> System.out.println("转换后: " + e))
                         .collect(Collectors.toList());

System.out.println("最终结果: " + result);

3.5 注意事项

使用Stream中间操作的注意事项:

  1. 中间方法返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程
  2. 修改Stream流中的数据,不会影响原来集合或者数组中的数据

四、Stream流的终结方法

终结方法会触发流的计算,并产生结果。流在终结操作后不能再被使用。

Stream的终结方法包括:

名称 说明
void forEach(Consumer action) 遍历
long count() 统计
toArray() 收集流中的数据,放到数组中
collect(Collector collector) 收集流中的数据,放到集合中
findFirst()/findAny() 查找第一个/任意一个元素
anyMatch()/allMatch()/noneMatch() 匹配元素
max()/min() 获取最大/最小值
reduce() 归约操作

4.1 遍历操作

List names = Arrays.asList("小明", "小红", "小刚");

// forEach: 遍历元素
names.stream().forEach(name -> System.out.println("姓名: " + name));

// 也可以使用增强for循环,效果相同
for (String name : names) {
    System.out.println("姓名: " + name);
}

4.2 计数操作

List names = Arrays.asList("小明", "小红", "小刚");

// count: 计数
long count = names.stream().count();
System.out.println("总人数: " + count);  // 3

4.3 收集操作

List names = Arrays.asList("小明", "小红", "小刚");

// toArray: 收集到数组
String[] nameArray = names.stream().toArray(size -> new String[size]);
System.out.println("数组长度: " + nameArray.length);  // 3

// collect: 收集到集合(List)
// collect(Collector collector)  收集流中的数据,放到集合中 (List Set Map)
List nameList = names.stream().collect(Collectors.toList());
System.out.println("列表: " + nameList);  // [小明, 小红, 小刚]

// collect: 收集到集合(Set)
Set nameSet = names.stream().collect(Collectors.toSet());
System.out.println("集合: " + nameSet);  // [小明, 小红, 小刚]

4.4 匹配操作

List numbers = Arrays.asList(1, 2, 3, 4, 5);

// anyMatch: 是否有任何元素匹配(有一个匹配就返回true)
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println("有偶数? " + hasEven);  // true

// allMatch: 是否所有元素都匹配
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
System.out.println("全是正数? " + allPositive);  // true

// noneMatch: 是否没有元素匹配
boolean noNegative = numbers.stream().noneMatch(n -> n < 0);
System.out.println("没有负数? " + noNegative);  // true

// 实际应用:检查集合中是否存在特定条件的元素
List persons = Arrays.asList(
    new Person("小明", 17),
    new Person("小红", 23),
    new Person("小刚", 28)
);

boolean hasMinor = persons.stream().anyMatch(p -> p.getAge() < 18);
System.out.println("有未成年人? " + hasMinor);  // true

boolean allAdult = persons.stream().allMatch(p -> p.getAge() >= 18);
System.out.println("全是成年人? " + allAdult);  // false

4.5 查找操作

List numbers = Arrays.asList(1, 2, 3, 4, 5);

// findFirst: 找到第一个元素
Optional first = numbers.stream().findFirst();
System.out.println("第一个元素: " + first.orElse(0));  // 1

// findAny: 找到任意一个元素(并行流中很有用)
Optional any = numbers.stream().findAny();
System.out.println("任意元素: " + any.orElse(0));  // 通常是1

// 结合filter使用
Optional firstEven = numbers.stream()
                                    .filter(n -> n % 2 == 0)
                                    .findFirst();
System.out.println("第一个偶数: " + firstEven.orElse(0));  // 2

4.6 归约操作(reduce)

归约操作将流中的元素组合起来,得到一个值。

List numbers = Arrays.asList(1, 2, 3, 4, 5);

// 求和
Optional sum = numbers.stream()
                              .reduce((a, b) -> a + b);
System.out.println("总和: " + sum.orElse(0));  // 15

// 指定初始值的reduce
int sumWithInitial = numbers.stream()
                           .reduce(10, (a, b) -> a + b);
System.out.println("带初始值的总和: " + sumWithInitial);  // 25

// 计算最大值
Optional max = numbers.stream()
                              .reduce((a, b) -> a > b ? a : b);
System.out.println("最大值: " + max.orElse(0));  // 5

// 计算字符串连接
String concatStr = Stream.of("A", "B", "C")
                         .reduce("", (a, b) -> a + b);
System.out.println("连接结果: " + concatStr);  // ABC

// 计算乘积
Optional product = numbers.stream()
                                 .reduce((a, b) -> a * b);
System.out.println("乘积: " + product.orElse(0));  // 120

4.7 最大值和最小值

List numbers = Arrays.asList(5, 3, 9, 1, 7);

// 获取最大值
Optional max = numbers.stream().max((a, b) -> a.compareTo(b));
System.out.println("最大值: " + max.orElse(0));  // 9

// 获取最小值
Optional min = numbers.stream().min((a, b) -> a.compareTo(b));
System.out.println("最小值: " + min.orElse(0));  // 1

// 在实际对象中使用
List people = Arrays.asList(
    "小明-25", 
    "小红-30", 
    "小刚-22"
);

// 获取年龄最大的人
Optional oldest = people.stream()
    .max((p1, p2) -> {
        int age1 = Integer.parseInt(p1.split("-")[1]);
        int age2 = Integer.parseInt(p2.split("-")[1]);
        return age1 - age2;
    });
System.out.println("年龄最大的人: " + oldest.orElse("无"));  // 小红-30

五、collect收集器详解

collect是一个非常强大的终端操作,通过Collectors类提供的工厂方法,我们可以将流转换为各种集合。

5.1 收集到List、Set、Map

// 假设有一个包含多个人名的列表
ArrayList list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 收集到List
// 需求:收集所有男性的名字到List中
List nameList = list.stream()
                         .filter(s -> "男".equals(s.split("-")[1]))
                         .collect(Collectors.toList());

// 收集到Set
// 需求:收集所有男性的名字到Set中
Set nameSet = list.stream()
                        .filter(s -> "男".equals(s.split("-")[1]))
                        .collect(Collectors.toSet());
System.out.println(nameSet);

// 收集到Map
// 收集到Map,key为姓名,value为年龄
Map nameToAge = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toMap(
        s -> s.split("-")[0],    // Key映射函数:获取姓名
        s -> Integer.parseInt(s.split("-")[2])    // Value映射函数:获取年龄
    ));
System.out.println(nameToAge);  
// {刘德华=50, 杜子腾=15, 吴京=37, 风清扬=20, 黄渤=40, 成龙=61}

使用Collectors.toMap时,需要提供两个Function

  1. 第一个Function用于指定如何从流中的元素获取Map的键(key)
  2. 第二个Function用于指定如何从流中的元素获取Map的值(value)

5.2 收集并分组

// 使用之前的人员列表
ArrayList list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 按性别分组
Map> personsByGender = list.stream()
    .collect(Collectors.groupingBy(s -> s.split("-")[1]));

System.out.println("男性列表: " + personsByGender.get("男").size());  // 6
System.out.println("女性列表: " + personsByGender.get("女").size());  // 3

// 按年龄段分组
Map> personsByAgeGroup = list.stream()
    .collect(Collectors.groupingBy(s -> {
        int age = Integer.parseInt(s.split("-")[2]);
        if (age < 18) return "未成年";
        else if (age < 60) return "中年";
        else return "老年";
    }));

System.out.println("未成年人: " + personsByAgeGroup.get("未成年").size());
System.out.println("中年人: " + personsByAgeGroup.get("中年").size());
System.out.println("老年人: " + personsByAgeGroup.get("老年").size());

5.3 其他常用收集操作

// 使用之前的人员列表
ArrayList list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 计算所有人的平均年龄
double avgAge = list.stream()
                  .mapToInt(s -> Integer.parseInt(s.split("-")[2]))
                  .average()
                  .orElse(0);
System.out.println("平均年龄: " + avgAge);

// 计算男性的平均年龄
double maleAvgAge = list.stream()
                      .filter(s -> "男".equals(s.split("-")[1]))
                      .mapToInt(s -> Integer.parseInt(s.split("-")[2]))
                      .average()
                      .orElse(0);
System.out.println("男性平均年龄: " + maleAvgAge);

// 连接所有人的姓名
String nameString = list.stream()
                      .map(s -> s.split("-")[0])
                      .collect(Collectors.joining(", "));
System.out.println("所有姓名: " + nameString);
// 杜子腾, 柳岩, 范冰冰, 风清扬, 刘德华, 黄渤, 赵丽颖, 吴京, 成龙

六、Stream流的实际应用案例

6.1 案例一:过滤和转换

// 基于之前的人员列表
ArrayList list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 需求:筛选出年龄大于35岁的男性,并提取他们的姓名
List olderMaleNames = list.stream()
                                .filter(s -> "男".equals(s.split("-")[1]))
                                .filter(s -> Integer.parseInt(s.split("-")[2]) > 35)
                                .map(s -> s.split("-")[0])
                                .collect(Collectors.toList());
System.out.println("35岁以上男性: " + olderMaleNames);  
// [刘德华, 黄渤, 吴京, 成龙]

6.2 案例二:统计和聚合

// 基于之前的人员列表
ArrayList list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 需求:计算男性和女性的平均年龄
Map averageAgeByGender = list.stream()
    .collect(Collectors.groupingBy(
        s -> s.split("-")[1],
        Collectors.averagingInt(s -> Integer.parseInt(s.split("-")[2]))
    ));

System.out.println("男性平均年龄: " + averageAgeByGender.get("男"));
System.out.println("女性平均年龄: " + averageAgeByGender.get("女"));

// 需求:找出年龄最大和最小的人
String oldestPerson = list.stream()
    .max((s1, s2) -> {
        int age1 = Integer.parseInt(s1.split("-")[2]);
        int age2 = Integer.parseInt(s2.split("-")[2]);
        return age1 - age2;
    })
    .orElse("无");

String youngestPerson = list.stream()
    .min((s1, s2) -> {
        int age1 = Integer.parseInt(s1.split("-")[2]);
        int age2 = Integer.parseInt(s2.split("-")[2]);
        return age1 - age2;
    })
    .orElse("无");

System.out.println("年龄最大的人: " + oldestPerson.split("-")[0] + 
                  ", 年龄: " + oldestPerson.split("-")[2]);
System.out.println("年龄最小的人: " + youngestPerson.split("-")[0] + 
                  ", 年龄: " + youngestPerson.split("-")[2]);

6.3 案例三:排序和限制

// 基于之前的人员列表
ArrayList list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 需求:按年龄排序,取出年龄最大的3个人
List top3ByAge = list.stream()
    .sorted((s1, s2) -> {
        int age1 = Integer.parseInt(s1.split("-")[2]);
        int age2 = Integer.parseInt(s2.split("-")[2]);
        return age2 - age1; // 降序排列
    })
    .limit(3)
    .collect(Collectors.toList());

System.out.println("年龄最大的三个人:");
top3ByAge.forEach(s -> {
    String[] parts = s.split("-");
    System.out.println(parts[0] + ", 年龄: " + parts[2]);
});
// 输出:
// 成龙, 年龄: 61
// 刘德华, 年龄: 50
// 黄渤, 年龄: 40

// 需求:按姓名长度排序
List sortedByNameLength = list.stream()
    .sorted((s1, s2) -> {
        String name1 = s1.split("-")[0];
        String name2 = s2.split("-")[0];
        return name1.length() - name2.length(); // 按姓名长度升序排列
    })
    .collect(Collectors.toList());

System.out.println("按姓名长度排序:");
sortedByNameLength.forEach(s -> {
    String[] parts = s.split("-");
    System.out.println(parts[0] + ", 长度: " + parts[0].length());
});

七、并行流

并行流允许Stream并行执行操作,可以更好地利用多核处理器。

7.1 创建并行流

// 方式1:从集合直接创建并行流
List numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream parallelStream = numbers.parallelStream();

// 方式2:将顺序流转换为并行流
Stream parallel = numbers.stream().parallel();

7.2 并行流示例

// 并行计算1到100万的和
long start = System.currentTimeMillis();
// 顺序流
long sum1 = IntStream.rangeClosed(1, 10_000_000)
                    .sum();
long end1 = System.currentTimeMillis();
System.out.println("顺序流耗时: " + (end1 - start) + "ms");

// 并行流
long start2 = System.currentTimeMillis();
long sum2 = IntStream.rangeClosed(1, 10_000_000)
                    .parallel()
                    .sum();
long end2 = System.currentTimeMillis();
System.out.println("并行流耗时: " + (end2 - start2) + "ms");

// 验证结果一致
System.out.println("结果一致: " + (sum1 == sum2));

7.3 并行流注意事项

  • 并行不一定更快,小数据量可能更慢(有线程创建和管理开销)
  • 确保操作是线程安全的(尤其是有状态操作)
  • 使用测试来确定是否值得使用并行流
  • 某些操作(如limit、findFirst)在并行流上可能比顺序流更慢,因为它们需要保持顺序
// 注意以下代码可能导致问题
List result = new ArrayList<>();

// 错误:并行流中的非线程安全操作
numbers.parallelStream().forEach(n -> result.add(n)); // 可能出现并发问题

// 正确:使用线程安全的收集操作
List safeResult = numbers.parallelStream()
                                .collect(Collectors.toList()); // 线程安全

八、常见问题与注意事项

8.1 Stream的一次性使用

Stream只能使用一次,否则会抛出异常:

Stream stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println);
// 下行代码会抛出异常
stream.forEach(System.out::println); // IllegalStateException: stream has already been operated upon or closed

正确的做法是重新获取流:

List list = Arrays.asList("a", "b", "c");
// 第一次使用
list.stream().forEach(s -> System.out.println(s));
// 需要再次使用,重新获取流
list.stream().forEach(s -> System.out.println(s));

8.2 延迟执行特性

中间操作是延迟执行的,只有在终端操作调用时才会执行:

List names = Arrays.asList("小明", "小红", "小刚");

// 创建流和定义中间操作
Stream stream = names.stream()
                           .filter(name -> {
                               System.out.println("过滤: " + name);
                               return name.length() > 1;
                           });
                           
System.out.println("流创建完毕,但未执行过滤操作");
// 此时没有任何输出,因为filter是中间操作,延迟执行

// 执行终端操作
List result = stream.collect(Collectors.toList());
// 现在会输出
// 过滤: 小明
// 过滤: 小红
// 过滤: 小刚

8.3 避免副作用

Stream操作应避免修改外部状态(副作用),这会导致不可预测的行为,特别是在并行流中。

// 错误示例:有副作用
List result = new ArrayList<>();
Stream.of("a", "b", "c").forEach(s -> result.add(s)); // 不推荐,有副作用

// 正确示例:无副作用
List betterResult = Stream.of("a", "b", "c").collect(Collectors.toList());

// 错误示例:修改外部变量
int[] sum = {0};
Stream.of(1, 2, 3).forEach(i -> sum[0] += i); // 不推荐,特别是在并行流中

// 正确示例:使用reduce
int betterSum = Stream.of(1, 2, 3).reduce(0, (a, b) -> a + b);

8.4 调试Stream流

调试Stream流可能很困难,使用peek操作可以帮助观察中间结果。

List result = Stream.of(1, 2, 3, 4, 5)
                           .peek(n -> System.out.println("原始值: " + n))
                           .filter(n -> n % 2 == 0)
                           .peek(n -> System.out.println("过滤后: " + n))
                           .map(n -> n * 2)
                           .peek(n -> System.out.println("映射后: " + n))
                           .collect(Collectors.toList());
System.out.println("最终结果: " + result);

8.5 性能考虑

Stream操作的性能与数据量、操作复杂性有关:

  1. 短路操作:尽可能使用短路操作(如findFirst, anyMatch)来提升性能
  2. 并行流:只在数据量大且操作独立时使用并行流
  3. 避免装箱拆箱:使用IntStream, LongStream等特化流避免装箱/拆箱操作
  4. 惰性操作:利用流的延迟执行特性进行优化
// 使用特化流避免装箱/拆箱
int sum = IntStream.range(1, 1000).sum(); // 比 boxed stream 更高效

// 利用短路操作
Optional first = Stream.of(1, 2, 3, 4, 5)
                               .filter(n -> {
                                   System.out.println("过滤: " + n);
                                   return n > 3;
                               })
                               .findFirst(); // 短路,不会处理所有元素

九、面试常见问题

9.1 Stream的特点是什么?

  • Stream不存储数据,而是按需计算
  • Stream操作不会修改源数据
  • Stream有延迟执行特性(惰性求值)
  • Stream可以是无限的
  • Stream只能遍历一次
  • Stream可以并行处理

9.2 Stream流、Collection和数组的区别?

  • Collection是一种数据结构,用于存储数据
  • 数组是固定大小的数据结构
  • Stream是用于处理数据的API,不存储数据

9.3 介绍一下Stream流的工作流程

  1. 创建流:从数据源获取流
  2. 中间操作:对流进行一系列转换和处理(延迟执行)
  3. 终端操作:触发流的计算,返回结果

9.4 什么是中间操作和终端操作?

  • 中间操作:返回新Stream的操作,可以链式调用,不会立即执行
  • 终端操作:返回非Stream的操作,会触发流的执行,一个流只能有一个终端操作

9.5 并行流和顺序流有什么区别?

  • 顺序流:在单个线程上按顺序处理元素
  • 并行流:利用多核处理器的优势,将流分割成多个部分,并行处理

十、总结

Java 8的Stream API为集合处理提供了强大的功能,使代码更简洁易读。

核心概念回顾

  1. Stream流的三个步骤:创建流、中间操作、终端操作
  2. 中间操作:filter、map、sorted等,延迟执行
  3. 终端操作:forEach、collect、count等,触发流执行

实用建议

  • 学习并熟练运用Stream API,可以大大提高代码质量和开发效率
  • 合理使用并行流,但需谨慎评估性能收益
  • 避免在Stream操作中产生副作用
  • 利用丰富的Collectors工具类处理复杂的收集需求

Stream API是现代Java编程的重要组成部分,掌握它可以让你的代码更具表达力和可维护性。

你可能感兴趣的:(JAVASE,开发语言,java)