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

目录

引言:为什么需要Stream流

一、Stream流基础

1.1 什么是Stream流

1.2 Stream流与集合的区别

1.3 Stream流的使用步骤

二、创建Stream流

2.1 从不同数据源创建Stream

2.1.1 从单列集合创建Stream(最常用)

2.1.2 从双列集合创建Stream

2.1.3 从数组创建Stream

2.1.4 从零散数据创建Stream

三、Stream流的中间操作

3.1 筛选和切片操作

3.2 映射操作

3.3 排序操作

3.4 查看操作(peek)

3.5 注意事项

四、Stream流的终结方法

4.1 遍历操作

4.2 计数操作

4.3 收集操作

4.4 匹配操作

4.5 查找操作

4.6 归约操作(reduce)

4.7 最大值和最小值

五、collect收集器详解

5.1 收集到List、Set、Map

5.2 收集并分组

5.3 其他常用收集操作

六、Stream流的实际应用案例

6.1 案例一:过滤和转换

6.2 案例二:统计和聚合

6.3 案例三:排序和限制

七、并行流

7.1 创建并行流

7.2 并行流示例

7.3 并行流注意事项

八、常见问题与注意事项

8.1 Stream的一次性使用

8.2 延迟执行特性

8.3 避免副作用

8.4 调试Stream流

8.5 性能考虑

九、面试常见问题

9.1 Stream的特点是什么?

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

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

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

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

十、总结


引言:为什么需要Stream流

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

List<Apple> 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就像一条流水线,专注于对数据进行操作
特性集合 CollectionStream流
存储方式存储元素不存储元素
操作时机立即执行延迟执行(惰性)
遍历次数可多次遍历只能遍历一次
关注点数据计算

1.3 Stream流的使用步骤

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

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

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

二、创建Stream流

2.1 从不同数据源创建Stream

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

获取方式方法名说明
单列集合default Stream<E> stream()Collection中的默认方法
双列集合无法直接使用stream流
数组public static <T> Stream<T> stream(T[] array)Arrays工具类中的静态方法
一堆零散数据public static<T> Stream<T> of(T... values)Stream接口中的静态方法
2.1.1 从单列集合创建Stream(最常用)
// 1.单列集合获取Stream流
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");
// 获取一条流水线,并把集合中的数据放到流水线上
Stream<String> stream1 = list.stream();
// 使用终结方法打印一下流水线上的所有数据
stream1.forEach(new Consumer<String>() {
    @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<String, Integer> 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<T> Stream<T> 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<T, R> mapper)转换流中的数据类型

3.1 筛选和切片操作

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

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

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

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

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

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

3.2 映射操作

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

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

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

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

// flatMap实际应用:提取所有单词的字符
List<String> words2 = Arrays.asList("Hello", "World");
List<String> 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<String> names = Arrays.asList("小明", "小红", "小刚", "小华");

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

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

// 逆序排序
List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 4);
List<Integer> 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<Person> people = Arrays.asList(
    new Person("小明", 20),
    new Person("小红", 25),
    new Person("小刚", 20)
);

// 先按年龄排序,年龄相同按姓名排序
List<Person> 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<String> 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<String> names = Arrays.asList("小明", "小红", "小刚");

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

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

4.2 计数操作

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

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

4.3 收集操作

List<String> 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<String> nameList = names.stream().collect(Collectors.toList());
System.out.println("列表: " + nameList);  // [小明, 小红, 小刚]

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

4.4 匹配操作

List<Integer> 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<Person> 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<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

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

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

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

4.6 归约操作(reduce)

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

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

// 求和
Optional<Integer> 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<Integer> 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<Integer> product = numbers.stream()
                                 .reduce((a, b) -> a * b);
System.out.println("乘积: " + product.orElse(0));  // 120

4.7 最大值和最小值

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

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

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

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

// 获取年龄最大的人
Optional<String> 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<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

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

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

// 收集到Map
// 收集到Map,key为姓名,value为年龄
Map<String, Integer> 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<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 按性别分组
Map<String, List<String>> 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<String, List<String>> 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<String> 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<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 需求:筛选出年龄大于35岁的男性,并提取他们的姓名
List<String> 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<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 需求:计算男性和女性的平均年龄
Map<String, Double> 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<String> list = new ArrayList<>();
Collections.addAll(list, "杜子腾-男-15", "柳岩-女-14", "范冰冰-女-13", "风清扬-男-20",
                 "刘德华-男-50", "黄渤-男-40", "赵丽颖-女-35", "吴京-男-37", "成龙-男-61");

// 需求:按年龄排序,取出年龄最大的3个人
List<String> 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<String> 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<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> parallelStream = numbers.parallelStream();

// 方式2:将顺序流转换为并行流
Stream<Integer> 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<Integer> result = new ArrayList<>();

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

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

八、常见问题与注意事项

8.1 Stream的一次性使用

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

Stream<String> 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<String> 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<String> names = Arrays.asList("小明", "小红", "小刚");

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

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

8.3 避免副作用

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

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

// 正确示例:无副作用
List<String> 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<Integer> 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<Integer> 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编程的重要组成部分,掌握它可以让你的代码更具表达力和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值