一、Java 8 Stream流简介
在Java 8中,Stream API是一个新添加的功能,它允许在集合上进行更加高效且便捷的操作。Stream API利用内部迭代器,以函数式编程的方式对集合进行处理,可以显著地提高代码的可读性和简洁性。
1.1 什么是Stream流
1.1.1 Stream流的定义
Stream流是一个来自数据源(例如集合、数组、I/O通道等)的元素序列,并支持聚合操作,可以让我们非常方便地对数据进行操作和处理。Stream API提供了一种统一的处理流式数据的方式,使得我们可以在不同的数据集合上使用相同的语法进行处理。
Stream流提供了一种高效且易于使用的处理数据的方式,特别是对于大量数据的处理。Stream流不是数据结构,而是对数据的一种描述,它不会存储数据,也不会修改数据源。
1.1.2 Stream流与传统集合的区别
- Stream流是一种数据流,不是数据结构,它不会存储数据。
- Stream流操作是延迟执行的,只有当需要结果时才会执行。
- Stream流可以进行并行处理,提高数据处理效率。
- Stream流提供了丰富的函数式编程方法,使得代码更简洁、易读。
1.2 Stream流的来源
1.2.1 从集合创建Stream流
集合类(如List和Set)可以通过调用stream()
方法创建一个Stream流。例如:
List<String> list = Arrays.asList("apple", "banana", "orange");
Stream<String> stream = list.stream();
1.2.2 从数组创建Stream流
可以使用Arrays.stream()
方法从数组创建一个Stream流。例如:
String[] array = {
"apple", "banana", "orange"};
Stream<String> stream = Arrays.stream(array);
1.2.3 从I/O通道创建Stream流
可以使用Files.lines()
方法从文件中创建一个Stream流,每个元素代表文件中的一行。例如:
Path path = Paths.get("file.txt");
try (Stream<String> stream = Files.lines(path)) {
stream.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
1.2.4 其他Stream流创建方法
-
使用
Stream.of()
方法创建一个包含多个元素的Stream流:Stream<String> stream = Stream.of("apple", "banana", "orange");
-
使用
Stream.iterate()
方法创建一个无限Stream流:Stream<Integer> stream = Stream.iterate(0, n -> n + 2).limit(10);
-
使用
Stream.generate()
方法创建一个无限Stream流:Stream<Double> stream = Stream.generate(Math::random).limit(5);
注意:在使用无限Stream流时,通常需要使用limit()
方法限制元素数量,以避免无限循环。
二、Stream流的操作
2.1 中间操作
2.1.1 filter过滤操作
filter
方法接受一个Predicate
类型的参数,用于过滤Stream中的元素,并返回符合条件的元素组成的新Stream。
/**
* 过滤所有偶数并输出
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
// 输出结果:
// 2
// 4
// 6
// 8
// 10
2.1.2 map映射操作
map
方法接受一个Function
类型的参数,用于将一个元素转换为另一个元素,并返回一个新的Stream。
/**
* 转换所有数字为它们的平方,并输出
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.map(n -> n * n)
.forEach(System.out::println);
// 输出结果:
// 1
// 4
// 9
// 16
// 25
2.1.3 flatMap扁平化操作
flatMap
方法与map
方法类似,不同之处在于flatMap
方法的参数是一个函数,该函数将一个元素映射成一个Stream,最终将这些Stream合并成一个新的Stream。
/**
* 将一个字符串数组分割成单词并输出
*/
String[] words = {"Hello", "World"};
Arrays.stream(words)
.flatMap(s -> Stream.of(s.split("")))
.forEach(System.out::println);
// 输出结果:
// H
// e
// l
// l
// o
// W
// o
// r
// l
// d
2.1.4 distinct去重操作
distinct
方法会返回一个去除重复元素后的新Stream。
/**
* 去掉重复数字并输出
*/
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5, 5);
numbers.stream()
.distinct()
.forEach(System.out::println);
// 输出结果:
// 1
// 2
// 3
// 4
// 5
2.1.5 sorted排序操作
sorted
方法接受一个可选的比较器,将Stream中的元素按照指定方式(默认为自然顺序)进行排序,返回一个新的Stream。
/**
* 对数字列表进行排序后输出
*/
List<Integer> numbers = Arrays.asList(5, 4, 3, 2, 1);
numbers.stream()
.sorted()
.forEach(System.out::println);
// 输出结果:
// 1
// 2
// 3
// 4
// 5
可以通过传递一个比较器来指定排序方式:
/**
* 对字符串列表以长度进行排序后输出
*/
List<String> words = Arrays.asList("apple", "banana", "pear", "orange");
words.stream()
.sorted(Comparator.comparing(String::length))
.forEach(System.out::println);
// 输出结果:
// pear
// apple
// banana
// orange
2.1.6 peek查看操作
peek
方法接受一个Consumer
类型的参数,可以在Stream中的每个元素执行所提供的操作,该方法不会影响Stream的元素。
/**
* 查看数字是否大于3,并输出
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.peek(n -> System.out.print("value: " + n + ", "))
.filter(n -> n > 3)
.forEach(System.out::println);
// 输出结果:
// value: 1, value: 2, value: 3, value: 4, 4
// value: 5, 5
2.1.7 limit截取操作
limit
方法用于截取Stream中指定数量的元素,返回一个新的Stream。
/**
* 只输出前3个数字
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.limit(3)
.forEach(System.out::println);
// 输出结果:
// 1
// 2
// 3
2.2 终止操作
2.2.1 forEach遍历操作
forEach
方法接受一个Consumer
类型的参数,对Stream中的每个元素执行所提供的操作。
/**
* 输出数字的平方
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.map(n -> n * n)
.forEach(System.out::println);
// 输出结果:
// 1
// 4
// 9
// 16
// 25
2.2.2 toArray转换为数组操作
toArray
方法将Stream中的元素转换为一个数组。
/**
* 将数字转换为数组
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Integer[] numberArray = numbers.stream().toArray(Integer[]::new);
for (Integer number : numberArray) {
System.out.print(number + " ");
}
// 输出结果:
// 1 2 3 4 5
2.2.3 reduce规约操作
reduce
方法用于将Stream中的所有元素结合成一个结果。它接受一个BinaryOperator
类型的参数,该参数定义了对Stream中的元素进行连续计算的方式。
/**
* 计算数字总和
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> result = numbers.stream().reduce((a, b) -> a + b);
result.ifPresent(System.out::println);
// 输出结果:
// 15
2.2.4 collect收集操作
collect
方法将Stream中的元素收集到一个集合中。它接受一个Collector
类型的参数,该参数定义了如何收集Stream中的元素。
/**
* 将数字列表转换为Set并输出
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Set<Integer> numberSet = numbers.stream().collect(Collectors.toSet());
for (Integer number : numberSet) {
System.out.print(number + " ");
}
// 输出结果:
// 1 2 3 4 5
2.2.5 min、max、count等聚合操作
min
、max
、count
等方法都是求Stream中元素的一些统计信息。
/**
* 求最大值、最小值和数量
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = numbers.stream().max(Integer::compare);
Optional<Integer> min = numbers.stream().min(Integer::compare);
long count = numbers.stream().count();
System.out.println("max: " + max.get() + ", min: " + min.get() + ", count: " + count);
// 输出结果:
// max: 5, min: 1, count: 5
2.2.6 anyMatch、allMatch、noneMatch等匹配操作
anyMatch
、allMatch
、noneMatch
等方法用于判断Stream中的元素是否满足特定条件。
/**
* 判断是否存在偶数
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean hasEvenNumber = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println("Has even number: " + hasEvenNumber);
// 输出结果:
// Has even number: true
2.2.7 findFirst、findAny等查找操作
findFirst
、findAny
等方法用于返回Stream中的任意一个元素(如果存在)。
/**
* 返回数字列表中的任意一个数字
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> anyNumber = numbers.stream().findAny();
anyNumber.ifPresent(System.out::println);
// 输出结果:
// 1 (注意输出结果可能与本例不一致)
三、Stream流的并行处理
3.1 并行Stream流简介
3.1.1 什么是并行Stream流
Java 8
引入的Stream
API,提供了一种新的处理集合数据的方式。 Java 8
中提供了两种类型的Stream
:
sequentialStream
:适合在单个处理器上运行,它只能顺序地处理一个元素流;parallelStream
:适合于运行在多核处理器上,这样可以将单个流分成多个流进行并行处理。
并行Stream
是对顺序Stream
的扩展,可以提高大量数据的处理速度,从而增加程序的性能。
3.1.2 并行Stream流的优势
并行计算可以显著提高处理大量数据的效率和响应时间,特别是当需要大量计算或遍历操作时。
并行Stream流的另一个优势是不需要程序员编写额外的代码来进行多线程操作,因为Java会自动将Stream流转换为并行执行模式,并自动维护所有必要的线程池和同步操作等。
3.2 创建并行Stream流
3.2.1 从顺序Stream流创建并行Stream流
从顺序Stream
流创建并行Stream
非常容易,只需要使用parallel()
方法即可将顺序流转换为并行流。
/**
* 使用顺序流和并行流分别计算数字列表的求和结果
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum1 = numbers.stream().reduce(0, Integer::sum);
int sum2 = numbers.parallelStream().reduce(0, Integer::sum);
System.out.println("Sequential Stream Sum: " + sum1);
System.out.println("Parallel Stream Sum: " + sum2);
// 输出结果:
// Sequential Stream Sum: 15
// Parallel Stream Sum: 15
3.2.2 从集合创建并行Stream流
从集合创建并行Stream
也非常容易,只需要使用parallelStream()
方法即可创建并行Stream
。
/**
* 使用并行流打印所有元素
*/
List<String> words = Arrays.asList("Hello", "Stream", "API");
words.parallelStream()
.forEach(System.out::println);
// 输出结果(可能不是完全一致的顺序):
// API
// Stream
// Hello
3.3 并行Stream流的注意事项
3.3.1 线程安全问题
在使用并行Stream
时,需要注意线程安全问题。避免多个线程共享相同的状态或数据,因为并行操作每个子任务执行可能在不同线程中,需要记得同步访问共享数据。
/**
* 在并行流中使用共享变量,导致结果错误
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
AtomicInteger total = new AtomicInteger();
numbers.parallelStream()
.forEach(n -> total.addAndGet(n));
System.out.println("Total: " + total);
// 输出结果:
// Total: 15 (错误)
在上面的示例中,AtomicInteger
是线程安全的,因为它提供原子性操作,但是由于forEach()
方法产生多个线程,因此每个线程都独立地对共享变量进行增量更新,从而导致计算错误。
解决该问题有两种方式:
- 避免使用可变共享变量;
- 使用线程安全的类或同步控制。
/**
* 使用并行流计算数字总和
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream().reduce(0, Integer::sum);
System.out.println("Sum: " + sum);
// 输出结果:
// Sum: 15
3.3.2 有状态操作的影响
在并行操作中,比如求和操作需要将所有元素相加,是一种有状态的操作。这类操作需要进行更多的同步和数据移动,以便多个线程可以协作计算。
对于大量数据而言,这类消耗较小。但是处理小规模数据时,这会成为一个瓶颈,因为线程同步的开销可能比执行本身的花费更高。
/**
* 并行计算数字集合中大于5的元素的总和
*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream()
.filter(n -> n > 5)
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum);
// 输出结果:
// Sum: 40
3.3.3 并行Stream流的性能考虑
并行Stream
处理比较大的数据集可能会影响性能,因为多线程的开销可能会超过实际的计算时间。另外,在使用并行Stream
时还要考虑到下面这些问题:
- 数据大小:通常情况下,数据越大,使用并行
Stream
的性能提升就越明显; - 操作花费:当操作时间越长时,并行
Stream
的性能提升就越明显; - 数据结构:在处理数据时考虑它们的数据结构,设置合适的数据结构能够提高并行操作的效率;
- 程序员熟悉度:程序员需要有足够熟练的技能才能正确地使用
parallelStream()
。
4.1 示例1:数据筛选与统计
假设有一个员工列表,每个员工有姓名、部门和薪水三个属性。现在需要对这个员工列表进行筛选和统计操作。具体要求如下:
- 筛选出薪水大于5000的员工;
- 按照部门对员工进行分组,并计算每个部门的平均薪水和员工数量;
- 对符合条件的员工按照薪水从高到低进行排序;
- 计算所有员工的平均薪水和最高薪水。
4.1.1 准备数据
首先,我们需要准备一组员工数据来模拟实际情况。假设我们有以下员工数据:
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Sales", 6000),
new Employee("Bob", "Sales", 5000),
new Employee("Charlie", "HR", 5500),
new Employee("David", "HR", 6500),
new Employee("Ella", "IT", 7500),
new Employee("Frank", "IT", 7000),
new Employee("Grace", "HR", 4500)
);
其中,Employee
类的定义如下:
class Employee {
private String name;
private String department;
private int salary;
public Employee(String name, String department, int salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
// Getters and setters
}
4.1.2 筛选出符合条件的员工
使用filter()
方法对员工列表进行筛选,只保留薪水大于5000的员工:
List<Employee> highPaidEmployees = employees.stream()
.filter(e -> e.getSalary() > 5000)
.collect(Collectors.toList());
System.out.println("高薪员工:");
highPaidEmployees.forEach(System.out::println);
输出结果为:
高薪员工:
Employee{
name='Alice', department='Sales', salary=6000}
Employee{
name='Charlie', department='HR', salary=5500}
Employee{
name='David', department='HR', salary=6500}
Employee{
name='Ella', department='IT', salary=7500}
Employee{
name='Frank', department='IT', salary=7000}
4.1.3 对员工列表进行分组和聚合
使用groupingBy()
方法对员工列表进行分组,并使用mapping()
方法进行值的转换和计算操作。
Map<String, DoubleSummaryStatistics> statsByDepartment = employees