掘金 后端 ( ) • 2024-04-25 11:23

Stream APIJava 8 引入的,这是一种用于处理集合、数组或其他数据源的高效、功能强大且易于使用的高级抽象。Stream API 提供了一种声明式编程模型,允许开发者以一种类似SQL查询的方式对数据进行过滤映射聚合等操作。

特性

Stream是数据流的抽象,它不是数据结构,不存储数据,而是从支持数据处理操作的源(如集合、数组、I/O通道等)生成一系列元素。Stream专注于对数据的计算,而不是数据的存储。

  • 惰性求值(Lazy Evaluation) :Stream的许多操作(如filter、map等)都不会立即执行,而是等到真正需要结果时才进行计算,这称为惰性求值。这种机制有助于优化性能,特别是处理大数据集时,避免不必要的计算。
  • 可并行处理(Parallel Processing) :Stream支持并行处理,可以在多核CPU上并行执行操作,提高计算效率。通过调用parallel()方法可以将串行流转换为并行流。
  • 不可修改(Immutable) :Stream不支持修改操作,即不能添加、删除或替换其中的元素。这有助于避免数据竞争和同步问题,确保线程安全。
  • 内部迭代(Internal Iteration) :传统集合遍历通常采用外部迭代(如for-each循环),而Stream采用内部迭代,由Stream API自身负责遍历数据源,开发者只需关注如何对每个元素进行操作。

Stream创建

Stream可以从各种数据源创建,如集合、数组、生成器函数等:

从集合或数组创建

  • 集合:通过 Collection 接口的 stream() 方法。
  • 数组:通过 Arrays.stream() 方法。
// 从集合创建
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> streamFromList = list.stream();
​
// 从数组创建
String[] array = {"d", "e", "f"};
Stream<String> streamFromArray = Arrays.stream(array);

创建无限流

  • 通过 Stream 类的静态方法,如 iterategenerateconcat
// 无限流,生成一个无限序列的随机数
Stream<Double> randomStream = Stream.generate(Math::random);
​
// 无限流,生成一个无限序列的整数
Stream<Integer> infiniteStream = Stream.iterate(0, i -> i + 1);

创建空或单元素流

  • 使用 Stream.empty() 创建一个空的 Stream
  • 使用 Stream.of() 创建一个单元素的 Stream
// 创建一个空的Stream流
Stream<String> emptyStream = Stream.empty();
// 创建一个但元素的Stream流
Stream<String> singleElementStream = Stream.of("Java");

通过 Stream.Builder创建

  • 对于需要构建复杂 Stream 的情况,可以使用 Collectors.toCollection() 方法。
Stream.Builder<String> builder = Stream.builder();
builder.add("Java");
builder.add("Python");
builder.add("Scala");
Stream<String> streamFromBuilder = builder.build();

代码示例

// 创建一个 List 集合,存储常见的变成语言名称
List<String> languages = Arrays.asList("Java", "Python", "Go", "PHP", "JavaScript");
// 过滤并映射 字符串开口为‘J’的元素
Stream<String> filteredStream  = languages.stream()
    // 过滤出以 "J" 开头的语言
    .filter(language -> language.startsWith("J"))
    // 转换为大写
    .map(String::toUpperCase);
// 收集结果
List<String> uppercaseLanguages  = filteredStream.collect(Collectors.toList());
​
// 遍历输出结果
uppercaseLanguages.forEach(System.out::println);// 输出 JAVA JAVASCRIPT

示例解析

  • 首先创建了一个 List 集合,然后通过调用 stream() 方法创建了一个 Stream
  • 接着,我们使用 filter 方法过滤出以 "J" 开头的语言,然后使用 map 方法将它们转换为大写。
  • 最后,我们使用 collect 方法将结果收集到一个新的 List 中,并打印出来。

注意事项

  • 避免在循环中创建 Stream:由于 Stream 是不可重复消费的,所以在循环中创建 Stream 可能会导致意外的行为。
  • 使用终端操作结束 Stream:在执行了一系列中间操作后,你需要调用一个终端操作来结束 Stream 并触发计算。

Stream操作

Stream API提供了丰富的操作,可分为两大类:中间操作(Intermediate Operations)和终止操作(Terminal Operations)。

中间操作

中间操作通常用于数据的筛选、转换、排序等操作,并且是惰性求值的,即它们不会立即执行,只有在调用终端操作时才会触发实际的计算。以下是Java Stream中一些常用的中间操作

筛选(filter

  • 通过filter(Predicate p)方法,根据给定的谓词(Predicate)函数,筛选出满足条件的元素。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// 筛选偶数
Stream<Integer> evenNumberStream = numbers.stream().filter(n -> n % 2 == 0);
// 收集结果并转成List集合
List<Integer> evenNumbers = evenNumberStream.collect(Collectors.toList());
System.out.println(evenNumbers);// 输出:[2, 4, 6, 8]

映射(map

  • 通过map(Function<? super T, ? extends R> mapper)方法,将Stream中的每个元素应用给定的函数,生成新的Stream,其中包含函数返回的结果。
List<String> words = Arrays.asList("Java", "Kotlin", "C#", "Python", "JavaScript");
// 获取每个元素的长度
Stream<Integer> wordLengthStream = words.stream().map(String::length);
// 收集结果
List<Integer> wordLengthList = wordLengthStream.collect(Collectors.toList());
System.out.println(wordLengthList);//输出:[4, 6, 2, 6, 10]

扁平化(flatMap

  • 通过flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)方法,将每个元素转换为一个Stream,然后将所有生成的Stream连接成一个单一的Stream。常用于处理嵌套数据结构。
List<List<String>> nestedWords = Arrays.asList(
    Arrays.asList("apple", "banana"),
    Arrays.asList("cherry", "date")              
);
// 将两个Stream流合并成一个
Stream<String> flattenedWordsStream = nestedWords.stream()
    .flatMap(List::stream);
// 收集结果
List<String> flattenedWords = flattenedWordsStream.collect(Collectors.toList());
System.out.println(flattenedWords); // 输出: [apple, banana, cherry, date]

去重(distinct

  • 通过distinct()方法,去除流中的重复元素。
List<String> words = Arrays.asList("Java", "Kotlin", "C#", "Python", "Java");
// 去除集合中重复的元素
Stream<String> uniqueStream = words.stream().distinct();
// 收集结果
List<String> uniqueWords = uniqueStream.collect(Collectors.toList());
System.out.println(uniqueWords);// 输出:[Java, Kotlin, C#, Python]

排序(sorted

  • 通过sorted(Comparator<? super T> comparator)方法,自然排序(对于实现了Comparable接口的元素类型)或指定比较器排序。

自然排序

List<String> words = Arrays.asList("Java", "Kotlin", "C#", "Python","JavaScript", "PHP");
// 默认自然排序(按首字母排序)
Stream<String> sortedStream = words.stream().sorted();
// 收集结果
List<String> sortedWords = sortedStream.collect(Collectors.toList());
System.out.println(sortedWords); //输出:[C#, Java, JavaScript, Kotlin, PHP, Python]

自定义比较器排序

List<String> words = Arrays.asList("Java", "Kotlin", "C#", "Python","JavaScript", "PHP");
// 自定义比较器(按长度)
Comparator<String> byLength  = Comparator.comparingInt(String::length);
// 调用自定义比较器
Stream<String> sortedStream = words.stream().sorted(byLength);
// 收集结果
List<String> sortedWords = sortedStream.collect(Collectors.toList());
System.out.println(sortedWords);// 输出:[C#, PHP, Java, Kotlin, Python, JavaScript]

切片(Slicing

  • skip(long n):跳过前n个元素,返回剩余元素构成的新Stream。
  • limit(long maxSize):限制Stream的最大长度,保留前n个元素。
  • 作用:对Stream进行分页或取部分子集。
List<String> words = Arrays.asList("Java", "Kotlin", "C#", "Python","JavaScript", "PHP");
// 跳到第三个开始输出(就是跳过开头两个)
Stream<String> skipStream = words.stream().skip(2);
List<String> skipWords = skipStream.collect(Collectors.toList());
System.out.println(skipWords);//输出:[C#, Python, JavaScript, PHP]
​
// 保留前两个元素
Stream<String> limitStream = words.stream().limit(2);
List<String> limitWords = limitStream.collect(Collectors.toList());
System.out.println(limitWords);//输出:[Java, Kotlin]
​
// 模拟分页输出
int pageSize = 3; // 每页个数
int currentPage = 2; // 第2页
long skipCount = (currentPage - 1) * pageSize; // 跳过前一页的元素数量
Stream<String> slicinStream = words.stream()
    .skip(skipCount) // 有顺序要求,skip必须在前面
    .limit(pageSize);
List<String> slicinWords = slicinStream.collect(Collectors.toList());
System.out.println(slicinWords);//输出:

注意事项

  • 中间操作可以链式调用,形成复杂的数据处理链。每个中间操作返回一个新的流,而原始流保持不变。这种链式操作使得Stream API非常灵活和强大。
  • 中间操作是惰性求值的,它们不会立即执行,直到终端操作被调用。
  • 中间操作可以复用流,即同一个流可以被多次使用,每次都会返回一个新的流。
  • 过度使用中间操作可能会影响性能,因为它们会增加处理链的复杂性。

终端操作

Stream API 中的终端操作(Terminal Operations)是 Stream 操作链的结束,它们触发实际的计算并产生一个结果或副作用。终端操作可以是短路的(如 forEach),也可以是非短路的(如 reducecollect)。这些操作可以分为以下几个类别:

归约(Reduction) :归约操作将流中的所有元素合并为一个单一的值。常见的归约操作包括:

  • reduce(BinaryOperator<T>):使用给定的二元操作符将流元素累积起来,生成一个单一的结果。例如,计算所有元素的总和、求最大值或最小值等。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5,7,8,9);
// 集合中所有元素相加
Integer sum = numbers.stream().reduce(0, Integer::sum);
System.out.println(sum);//输出: 39
  • sum()max()min()average():针对数值型流的便捷归约操作,分别计算总和、最大值、最小值和平均值。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5,7,8,9);
double average = numbers.stream()
    .mapToInt(Integer::intValue)
    .average()
    .orElse(Double.NaN);
System.out.println(average);//输出: 4.875

解析

  • mapToInt() 方法。此方法将当前的 Stream<Integer> 转换为一个 IntStream,同时对流中的每个元素应用提供的函数。
  • Integer::intValue 表示将每个 Integer 元素直接转换为其原始的 int 值。
  • average() 方法,它是 IntStream 提供的一个终端操作,用于计算流中所有整数的平均值。
  • orElse() 方法处理其可能的未包含值情况。
  • Double.NaN,表示“非数字”(Not-a-Number)

收集(Collection) :这类操作将流中的元素收集到一个集合或其他可迭代结构中。

  • collect(Collector<? super T,A,R> collector),其中 Collector 是一个接口,定义了如何将流元素转换为一个汇总结果。Java标准库提供了多种预定义的 Collector 实现,如 toList()toSet()toMap() 等。
List<String> words = Arrays.asList("Java", "Kotlin", "C#", "Python","JavaScript", "PHP");
// List集合转 Set集合
Set<String> uniqueWords = words.stream().collect(Collectors.toSet());
System.out.println(uniqueWords);//输出: [C#, Java, JavaScript, PHP, Kotlin, Python]

遍历(Traversal) :这些操作主要用于对流中的每个元素执行某个动作,但不产生聚合结果。

  • 最典型的遍历操作是 forEach(Consumer<? super T> action),它接收一个 Consumer 函数并应用于流中的每个元素。
List<String> words = Arrays.asList("Java", "Kotlin", "C#", "Python","JavaScript", "PHP");
// 遍历集合的每个元素
words.stream().forEach(System.out::println);

匹配(Matching) :匹配操作检查流中的元素是否满足特定条件,并返回一个布尔值作为结果。常用的匹配操作包括:

  • anyMatch(Predicate<? super T> predicate):检查流中是否存在至少一个元素满足给定的谓词条件。
  • allMatch(Predicate<? super T> predicate):检查流中是否所有元素都满足给定的谓词条件。
  • noneMatch(Predicate<? super T> predicate):检查流中是否没有任何元素满足给定的谓词条件。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5,7,8,9);
// 判断集合中的元素是否有偶数
boolean hasEvenNumber  = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println(hasEvenNumber);// 输出: true

查找与统计(Find & Count) :查找操作返回流中的某个特定元素或满足特定条件的元素,统计操作则计算元素的数量。

  • findFirst()findAny():返回流中的第一个元素或任意一个元素(对于并行流而言)。
  • count():返回流中元素的总数。
  • distinct():返回由流中唯一元素组成的流,相当于去重操作。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5,7,8,9);
// 查找第一个偶数
Optional<Integer> firstEven = numbers.stream()
    .filter(n -> n % 2 == 0)
    .findFirst();
System.out.println(firstEven.orElse(-1));//输出: 2
​
// 获取奇数的个数
long oddCount = numbers.stream()
    .filter(n -> n % 2 != 0)
    .count();
 System.out.println(oddCount); // 输出:5

注意事项

  • Stream只能消费一次:一旦执行了终止操作,Stream就被视为消耗掉,无法再次使用。如果需要重复使用,需要重新创建Stream。
  • Stream操作是无状态的或有状态的:无状态操作(如filter, map)不依赖于Stream中的其他元素,而有状态操作(如sorted, distinct)需要查看所有元素才能做出决定。
  • 并行Stream的性能提升:并行Stream可以利用多核处理器提高计算速度,但并非所有操作都能从中受益,且可能会增加额外的开销。使用时应根据具体场景评估是否值得开启并行。