Java 8 Stream流:探索高效、简洁的数据处理之道

一、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等聚合操作

minmaxcount等方法都是求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等匹配操作

anyMatchallMatchnoneMatch等方法用于判断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等查找操作

findFirstfindAny等方法用于返回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()方法产生多个线程,因此每个线程都独立地对共享变量进行增量更新,从而导致计算错误。

解决该问题有两种方式:

  1. 避免使用可变共享变量;
  2. 使用线程安全的类或同步控制。
/**
 * 使用并行流计算数字总和
 */
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时还要考虑到下面这些问题:

  1. 数据大小:通常情况下,数据越大,使用并行Stream的性能提升就越明显;
  2. 操作花费:当操作时间越长时,并行Stream的性能提升就越明显;
  3. 数据结构:在处理数据时考虑它们的数据结构,设置合适的数据结构能够提高并行操作的效率;
  4. 程序员熟悉度:程序员需要有足够熟练的技能才能正确地使用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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值