Java8 Stream深度解析:30个案例3万字助你精通集合筛选、归约、分组与聚合操作

本文主要是介绍Java8 Stream深度解析:30个案例3万字助你精通集合筛选、归约、分组与聚合操作,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Java 8引入了Stream API,这是一个用于处理集合元素的强大工具。通过Stream流,你可以以一种声明式的方式处理数据,使得代码更加简洁、易读且易于维护。在本文中,我们将深入探讨Java8 Stream流的使用,包括其基本概念、常用操作以及实际应用示例。

一、Stream流的特点和使用流程

Stream API 是 Java 8 引入的一个新特性,它允许开发者以声明性方式处理数据集合(如列表和集合)。Stream API 可以简化复杂的数据操作,并且支持并行处理以提高性能。

以下是 Stream API 的主要特点和使用流程:

1. 特点:

  • 声明性: Stream API 允许你描述你想要做什么,而不是详细说明怎么做。
  • 链式操作: 你可以将多个操作链接在一起,形成一个流水线,每个操作都会生成一个新的流供下一个操作使用。
  • 函数式编程: Stream API 鼓励使用无副作用的函数和 lambda 表达式。
  • 并行处理: Stream API 支持并行流,可以充分利用多核处理器。
  • 延迟执行: Stream 操作是惰性的,只有在终端操作(如 collect、forEach)被调用时,整个流水线才会执行。
  • 短路操作: 某些终端操作(如 anyMatch、allMatch、noneMatch、findFirst)在找到结果后会立即停止处理。

2. 使用流程:

  • 创建流: 从数据源(如集合、数组、文件等)创建一个流。

List list = Arrays.asList(“a”, “b”, “c”); Stream
stream = list.stream();

  • 中间操作: 对流进行一系列转换操作,如 filter(过滤)、map(映射)、sorted(排序)等。这些操作会返回一个新的流,并且不会立即执行。
Stream<String> filteredStream = stream.filter(s -> s.startsWith("a"));
  • 终端操作: 执行一个终端操作来结束流的处理并产生结果。终端操作会触发整个流水线的执行,并且不会返回一个新的流。
List<String> result = filteredStream.collect(Collectors.toList());
  • 处理结果: 使用终端操作返回的结果进行后续处理。

result.forEach(System.out::println);

注意,流只能被使用一次。一旦终端操作被触发,流就会被关闭,无法再次使用。如果你需要再次处理数据,你需要重新创建一个新的流。

二、Stream流的魅力(综合示例)

先来一个综合案例,然后感受一下stream的魅力

以下是一个:分组、排序然后提取每组中最小和最大值的案例,我们来看一下使用stream和不使用stream的代码实现。

List<Integer> numbers = Arrays.asList(1, 5, 3, 8, 10, 2, 6, 7, 4, 9);  // 分组、排序并提取最小和最大值  Map<Boolean, List<Integer>> result = numbers.stream()  .collect(Collectors.groupingBy(n -> n % 2 == 0, // 分组:奇数和偶数  Collectors.collectingAndThen(  Collectors.toList(), // 收集到列表  list -> {  // 对列表进行排序  Collections.sort(list);  // 提取并返回最小和最大值return Lists.newArrayList(list.get(0),list.get(list.size() - 1));  }  
)));  

如果我们不使用stream,代码可能是这样的,头大吗?

import java.util.ArrayList;  
import java.util.Arrays;  
import java.util.Collections;  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  public class GroupAndSort {  public static void main(String[] args) {  List<Integer> numbers = Arrays.asList(1, 5, 3, 8, 10, 2, 6, 7, 4, 9);  Map<Boolean, List<Integer>> result = new HashMap<>();  result.put(true, new ArrayList<>()); // 偶数分组  result.put(false, new ArrayList<>()); // 奇数分组  for (Integer number : numbers) {  if (number % 2 == 0) {  result.get(true).add(number);  } else {  result.get(false).add(number);  }  }  // 对分组后的列表进行排序  Collections.sort(result.get(true));  Collections.sort(result.get(false));  // 提取并设置最小和最大值(注意这里需要使用get(list.size() - 1)来获取最大值)  List<Integer> evenResult = result.get(true);  if (evenResult.size() > 1) {  result.put(true, Arrays.asList(evenResult.get(0), evenResult.get(evenResult.size() - 1)));  } else if (evenResult.size() == 1) {  result.put(true, Arrays.asList(evenResult.get(0), evenResult.get(0))); // 如果只有一个元素,则最小值和最大值相同  } else {  result.put(true, Collections.emptyList()); // 如果没有元素,则为空列表  }  List<Integer> oddResult = result.get(false);  if (oddResult.size() > 1) {  result.put(false, Arrays.asList(oddResult.get(0), oddResult.get(oddResult.size() - 1)));  } else if (oddResult.size() == 1) {  result.put(false, Arrays.asList(oddResult.get(0), oddResult.get(0))); // 如果只有一个元素,则最小值和最大值相同  } else {  result.put(false, Collections.emptyList()); // 如果没有元素,则为空列表  }   }  
}

三、stream流的创建

Stream可以通过多种方式创建。以下是一些常见的创建Stream的方法:

1. 通过集合创建:

这是创建Stream最常用的方式之一。你可以通过调用集合对象的stream()方法来获取一个流。例如,如果你有一个List或Set,你可以直接调用它们的stream()方法来获取流。

List<String> list = Arrays.asList("a", "b", "c");  
Stream<String> streamFromList = list.stream();

2. 通过数组创建:

使用Arrays.stream()方法从数组创建流,这适用于任何类型的数组。

int[] array = {1, 2, 3, 4, 5};  
IntStream intStream = Arrays.stream(array);  // 或者对于对象数组  
String[] strArray = {"a", "b", "c"};  
Stream<String> stringStream = Arrays.stream(strArray);

注意,对于基本类型的数组,Arrays.stream()会返回特定类型的流,如IntStream、LongStream或DoubleStream。如果你需要将这些流转换为通用Stream,你可以使用boxed()方法。

3. 通过Stream的静态方法:

Stream类提供了几个静态方法来创建流。例如,Stream.of()方法可以接受一系列元素并创建一个流。

Stream<String> stream = Stream.of("a", "b", "c");

4. 通过随机数生成:

Random类可以用于生成随机数,并且你可以使用ints()、longs()或doubles()方法创建一个无限流。但是,你通常会结合使用limit()方法来限制流的长度。

Random random = new Random();  
IntStream randomIntStream = random.ints(10, 0, 100); // 生成10个0到100之间的随机数

5. 通过文件I/O:

在处理文件时,你可以使用Files类中的方法,如lines(),从文件中读取行并创建一个流。

Path path = Paths.get("path/to/file.txt");  
Stream<String> linesStream = Files.lines(path);

6. 通过其他数据源:

还有其他一些不太常见但有用的方法可以从其他数据源创建流,例如从BufferedReader、InputStream等。

7. 无限流:

Stream API 提供了能够生成无限序列的流

  • Stream.iterate(T seed, UnaryOperator f): 创建一个无限顺序的流,通过反复应用函数f来生成元素。
  • Stream.generate(Supplier s): 创建一个无限无序的流,其中每个元素由提供的Supplier生成。
// 使用iterate生成一个无限递增的整数流  
Stream<Integer> infiniteIntegerStream = Stream.iterate(0, i -> i + 1);  // 使用generate生成一个无限的随机数流  
Stream<Double> infiniteRandomStream = Stream.generate(Math::random);

8. 通过范围创建:

  • IntStream.range(int startInclusive, int endExclusive):
  • LongStream.range(int startInclusive, int endExclusive):

创建一个包含从startInclusive(包含)到endExclusive(不包含)之间的整数的流。

// 创建一个从0(包含)到10(不包含)的整数流  
IntStream intStream = IntStream.range(0, 10);  

请注意,无限流应该在使用时结合limit()或其他短路操作,以避免无限循环和处理过多的元素。

创建Stream的方式取决于你的具体需求和数据源。选择最适合你情况的方法来创建Stream可以使你的代码更加简洁和高效

四、Stream流的应用:

1. 中间操作:

1.1 Filter(过滤)/map(转换)/mapToInt/mapToDouble/mapToLong

filter方法用于过滤流中的元素,而map方法用于对流中的每个元素执行某种操作,并返回一个新的流。

下面是一个简要的例子,演示如何过滤出所有工资超过5000的员工,并将他们的名字映射到一个新的列表中:

import java.util.Arrays;  
import java.util.List;  
import java.util.stream.Collectors;  public class EmployeeFilterMapDemo {  public static void main(String[] args) {  // 创建一个员工列表:包含姓名和薪水两个属性  List<Employee> employees = Arrays.asList(  new Employee("Alice", 5500.0),  new Employee("Bob", 6000.0),  new Employee("Charlie", 4500.0),  new Employee("Diana", 5700.0)  );  // 使用Stream API的filter方法过滤出工资超过5000的员工,  // 然后使用map方法将每个员工映射成他们的名字,并收集到一个新的列表中  List<String> namesOfHighSalaryEmployees = employees.stream()  .filter(e -> e.getSalary() > 5000.0)  .map(Employee::getName)  .collect(Collectors.toList());  // 打印过滤并映射后的员工名字列表  System.out.println("Names of employees with salary > 5000: " + namesOfHighSalaryEmployees);  }  
}

此外streamAPI中还有指定类型的map方法:

  • mapToInt(ToIntFunction<? super T> mapper): 将流中的元素转换成int类型。
  • mapToLong(ToLongFunction<? super T> mapper): 将流中的元素转换成long类型。
  • mapToDouble(ToDoubleFunction<? super T> mapper): 将流中的元素转换成double类型。
1.2 flatMap(转换)

flatMap方法在Java Stream API中用于将流中的每个元素转换成一个新的流,然后将这些新生成的流合并成一个单一的流。这通常用于处理流中的集合或数组元素,以将它们“展平”成一个单一的元素流。

首先,定义一个包含字符串列表的列表,然后使用flatMap将其转换成一个包含所有字符串的单一流:

import java.util.Arrays;  
import java.util.List;  
import java.util.stream.Collectors;  public class FlatMapExample {  public static void main(String[] args) {  // 创建一个包含列表的列表  List<List<String>> listOfLists = Arrays.asList(  Arrays.asList("A", "B", "C"),  Arrays.asList("D", "E"),  Arrays.asList("F", "G", "H", "I")  );  // 使用flatMap将内部列表展平成一个单一列表  List<String> flatList = listOfLists.stream()  .flatMap(List::stream) // 使用List的stream方法将每个列表转换成流,然后合并  .collect(Collectors.toList());  // 打印结果  System.out.println(flatList);  }  
}
/// 输出结果将是:[A, B, C, D, E, F, G, H, I]

在这个例子中,我们有一个List<List>,我们使用stream()方法将其转换成一个流,然后通过flatMap和List::stream方法引用将每个内部列表转换成一个新的流,并将这些流合并成一个包含所有字符串的单一流。

1.3 Distinct(去重)

distinct 方法在 Java Stream API 中用于去除流中的重复元素,确保每个元素只出现一次。这个方法基于元素的 equals 和 hashCode 方法来确定哪些元素是重复的。

下面是一个使用 distinct 方法的简要示例,以及预期的输出结果:

import java.util.Arrays;  
import java.util.List;  
import java.util.stream.Collectors;  public class DistinctExample {  public static void main(String[] args) {  // 创建一个包含重复元素的列表  List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5, 5, 1);  // 使用distinct方法去除重复元素  List<Integer> distinctNumbers = numbers.stream()  .distinct()  .collect(Collectors.toList());  // 打印结果  System.out.println(distinctNumbers);  }  
}
// 输出结果将是:[1, 2, 3, 4, 5]

在这个例子中,我们有一个包含重复整数的列表。通过调用 stream() 方法将其转换成一个流,然后使用 distinct() 方法去除流中的重复元素

1.4 Limit(限制)/Skip(跳过)/Peek(展示)

limit用于限制流中的元素数量,skip用于跳过流中的前N个元素,而peek则允许对流中的每个元素执行某种操作(如打印、修改等)而不改变流本身。peek通常用于调试或查看流中的元素。

下面是一个使用limit、skip和peek方法的简要示例:

import java.util.Arrays;  
import java.util.List;  
import java.util.stream.Collectors;  public class LimitSkipPeekExample {  public static void main(String[] args) {  // 创建一个整数列表  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);  // 使用peek打印流中的元素,然后使用limit和skip获取特定元素  List<Integer> result = numbers.stream()  .peek(System.out::println) // 打印每个元素  .skip(2)                   // 跳过前两个元素  .limit(3)                  // 获取接下来的三个元素  .collect(Collectors.toList()); // 收集结果  // 打印最终结果  System.out.println("Result after skip and limit: " + result);  }  
}

输出结果将是:

1  
2  
3  
4  
5  
Result after skip and limit: [3, 4, 5]

需要注意的是,peek方法虽然执行了操作(在这里是打印),但它不会改变流中的元素或流的结构。此外,由于peek是一个中间操作,它不会立即执行,而是在终端操作(如collect)被调用时才执行。在这个例子中,peek的调用位于skip和limit之前,但由于流的惰性求值,实际上打印操作是在skip和limit之后执行的。

1.5 Sorted(排序)

排序可以通过sorted()方法实现,该方法有两种形式:

  • 无参的sorted(),它使用元素的自然顺序进行排序(要求元素实现Comparable接口);
  • 以及接受Comparator参数的sorted(Comparator<? super T> comparator),它允许你自定义排序规则。

下面是一个使用sorted()方法进行排序的简要示例:

import java.util.Arrays;  
import java.util.List;  
import java.util.stream.Collectors;  public class SortingExample {  public static void main(String[] args) {  // 创建一个字符串列表  List<String> words = Arrays.asList("banana", "apple", "cherry", "date", "elderberry");  // 使用sorted()方法按自然顺序排序  List<String> sortedWords = words.stream()  .sorted()  .collect(Collectors.toList());  // 打印排序后的结果  System.out.println("Sorted words in natural order: " + sortedWords);  // 创建一个整数列表  List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 7);  // 使用sorted()方法和自定义Comparator进行排序  List<Integer> sortedNumbers = numbers.stream()  .sorted((a, b) -> b - a) // 降序排序  .collect(Collectors.toList());  // 打印排序后的结果  System.out.println("Sorted numbers in descending order: " + sortedNumbers);  }  
}

输出结果将是:

Sorted words in natural order: [apple, banana, cherry, date, elderberry]  
Sorted numbers in descending order: [9, 7, 5, 2, 1]
1.6 concat(两个流连接成一个流)

concat(Stream<? extends T> a, Stream<? extends T> b): 静态方法,用于将两个流连接成一个流。

 // 创建两个整数列表  
List<Integer> list1 = Arrays.asList(1, 2, 3);  
List<Integer> list2 = Arrays.asList(4, 5, 6);  // 将两个列表转换为流,并使用 concat 方法连接它们  
Stream<Integer> concatenatedStream = Stream.concat(list1.stream(), list2.stream());  
// 使用连接后的流进行一些操作,比如打印所有元素  
concatenatedStream.forEach(System.out::println);  

2. 终端操作

2.1 forEach/findFirst/findAny

在Java Stream API中,forEach、findFirst和findAny都是终端操作,它们用于执行某种操作或检索流中的元素。forEach用于迭代流中的每个元素并执行一个操作,findFirst用于获取流中的第一个元素(如果存在的话),而findAny则用于获取流中的任意元素(对于并行流特别有用,因为它可能更快)。

下面是一个使用这些方法的简要示例:

import java.util.Arrays;  
import java.util.List;  
import java.util.Optional;  public class StreamMethodsExample {  public static void main(String[] args) {  // 创建一个整数列表  List<Integer> numbers = Arrays.asList(1, 2, 3);  // 使用forEach打印每个元素  numbers.stream()  .forEach(System.out::println);  // 使用findFirst获取第一个元素  Optional<Integer> firstNumber = numbers.stream()  .findFirst();  System.out.println("First number: " + firstNumber.orElse(null));  // 使用findAny获取任意元素  Optional<Integer> anyNumber = numbers.stream()  .findAny();  System.out.println("Any number: " + anyNumber.orElse(null));  }  
}

输出结果将是流中的每个数字,以及第一个数字和任意数字(在这种情况下,findAny可能返回流中的第一个元素,但在并行流中则不一定):

1  
2  
3  
First number: 1  
Any number: 1

请注意,findFirst和findAny返回的是一个Optional对象,这是因为流可能是空的,不包含任何元素。使用orElse方法可以提供一个默认值,以防Optional为空。在这个例子中,如果流为空,则输出null。

另外,虽然在这个顺序流的例子中findAny返回了第一个元素,但如果你在处理并行流,findAny可能会返回流中的任何其他元素,因为它旨在提高性能,而不是保证返回特定元素。

2.3 count/sum/max/min

count、sum、max和min都是终端操作,用于对流中的元素进行计数、求和、找最大值和最小值。这些操作在处理数值型流时特别有用。

下面是一个使用这些方法的简要示例:

// 创建一个整数列表  
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);  // 使用count计算元素数量  
long count = numbers.stream()  .count();  System.out.println("Count of elements: " + count);  // 使用sum计算元素总和  
int sum = numbers.stream()  .mapToInt(Integer::intValue) // 转换为IntStream以使用sum  .sum();  System.out.println("Sum of elements: " + sum);  // 使用max获取最大值  
OptionalDouble max = numbers.stream()  .mapToDouble(Integer::doubleValue) // 转换为DoubleStream以使用max  .max();  System.out.println("Max value: " + max.orElse(Double.NaN));  // 使用min获取最小值  
OptionalDouble min = numbers.stream()  .mapToDouble(Integer::doubleValue) // 转换为DoubleStream以使用min  .min();  System.out.println("Min value: " + min.orElse(Double.NaN));  

输出结果将是:

Count of elements: 10  
Sum of elements: 55  
Max value: 10.0  
Min value: 1.0

max和min方法返回OptionalDouble,这是因为流可能是空的。在这个例子中,我们知道流不是空的,所以我们可以安全地调用orElse方法并提供一个默认值(在这里是Double.NaN),但实际上这个默认值是不会被用到的,因为流中确实有元素。如果流为空,orElse方法将返回提供的默认值。

2.4 anyMatch/allMatch/noneMatch

anyMatch、allMatch和noneMatch是终端操作,用于检查流中的元素是否满足给定的谓词(条件)。anyMatch检查是否有任何元素满足条件,allMatch检查是否所有元素都满足条件,而noneMatch检查是否没有任何元素满足条件。

下面是一个使用这些方法的简要示例:

// 创建一个整数列表  
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);  // 使用anyMatch检查是否有任何偶数  
boolean hasEven = numbers.stream()  .anyMatch(n -> n % 2 == 0);  System.out.println("Has any even number? " + hasEven);  // 使用allMatch检查是否所有数字都小于11  
boolean allLessThan11 = numbers.stream()  .allMatch(n -> n < 11);  System.out.println("Are all numbers less than 11? " + allLessThan11);  // 使用noneMatch检查是否没有任何数字等于0  
boolean noZeros = numbers.stream()  .noneMatch(n -> n == 0);  System.out.println("Are there no zeros? " + noZeros);  

输出结果将是:

Has any even number? true  
Are all numbers less than 11? true  
Are there no zeros? true

这些方法在流处理中非常有用,因为它们允许你快速地对流中的元素进行条件检查,而无需显式地迭代每个元素。

2.5 归约reduce

reduce方法是一个终端操作,用于将流中的所有元素组合成一个单一的结果。它通常用于执行某种累积操作,比如计算元素的总和、乘积或连接字符串等。

  • reduce(T identity, BinaryOperator accumulator)
    此方法接受一个初始值和一个累积函数,用于归约流中的元素。
  • reduce(BinaryOperator accumulator)
    此方法不接受初始值,而是使用流中的第一个元素作为初始值,然后应用累积函数。

计算一个员工列表中所有员工的总薪水,同时找出薪水最高的员工。
首先,我们定义一个Employee类来表示员工:

public class Employee {  private String name;  private double salary;  ... 
}

然后,我们使用reduce方法来计算总薪水和找出薪水最高的员工:

// 创建一个员工列表  
List<Employee> employees = Arrays.asList(  new Employee("Alice", 5000.0),  new Employee("Bob", 6000.0),  new Employee("Charlie", 5500.0),  new Employee("David", 6500.0)  
);  // 使用初始值的 reduce 方法来计算所有员工的总薪水 
doubletotalSalary = employees.stream()  .reduce(0, (acc, employee) -> acc + employee.getSalary(), Integer::sum); 
System.out.println("Total salary of all employees: " + totalSalary);  
// 不使用初始值的 reduce 方法来连接所有员工的名字  
Optional<String> combinedNames = employees.stream()  .map(Employee::getName)  .reduce((name1, name2) -> name1 + ", " + name2);    
// 使用reduce找出薪水最高的员工  
Optional<Employee> highestPaidEmployee = employees.stream()  .reduce((emp1, emp2) -> emp1.getSalary() > emp2.getSalary() ? emp1 : emp2); System.out.println("Highest paid employee: " + highestPaidEmployee.orElse(null));  // 或者使用max方法找出薪水最高的员工(更简洁)  
Optional<Employee> highestPaidEmployeeWithMax = employees.stream()  .max(Comparator.comparingDouble(Employee::getSalary));  System.out.println("Highest paid employee (using max): " + highestPaidEmployeeWithMax.orElse(null));  

注意,reduce方法在这里返回的是一个Optional,因为流可能是空的。我们使用orElse方法来处理这种情况,如果流为空,则返回null。

最后,我们展示了如何使用max方法和Comparator.comparingDouble来更简洁地找出薪水最高的员工。这种方法通常更可取,因为它更清晰地表达了我们的意图,并且代码更简洁。

输出结果将是:

Total salary of all employees: 23000.0  
Highest paid employee: Employee{name='David', salary=6500.0}  
Highest paid employee (using max): Employee{name='David', salary=6500.0}

3. 收集操作

3.1 collect 收集(三个参数)

collect 方法在 Java Stream API 中通常用于收集流中的元素到某种集合或其他数据结构中。

  • collect(Supplier supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner):

这个特定重载版本的 collect 方法提供了更高的灵活性,允许你自定义收集过程。

这个 collect 方法接受三个参数:

  • Supplier supplier:一个供应器,用于创建新的结果容器。
  • BiConsumer<R, ? super T> accumulator:一个累加器,用于将流中的元素添加到结果容器中。
  • BiConsumer<R, R> combiner:一个组合器,用于合并两个结果容器(通常用于并行流)。

以下是一个使用这个 collect 方法的示例,我们将自定义一个收集过程,将流中的字符串连接成一个单独的字符串:

  // 创建一个字符串流  
Stream<String> stringStream = Stream.of("Hello", " ", "World", "!", " ", "Welcome", " ", "to", " ", "Java");  // 使用自定义的 collect 方法来连接字符串  
String concatenated = stringStream.collect(  // 供应器:创建一个 StringBuilder  StringBuilder::new,  // 累加器:将每个字符串添加到 StringBuilder  StringBuilder::append,  // 组合器:将两个 StringBuilder 合并(这里其实不需要,因为我们是顺序处理的,但为了示例完整性还是提供了)  (left, right) -> left.append(right)  ).toString();  // 输出结果  
System.out.println(concatenated);  

在这个例子中,我们使用了 StringBuilder 作为结果容器,因为字符串连接在 Java 中使用 StringBuilder 是更高效的。供应器 StringBuilder::new 创建了一个新的 StringBuilder 实例。累加器 StringBuilder::append 将流中的每个字符串添加到 StringBuilder 中。组合器 (left, right) -> left.append(right) 用于合并两个 StringBuilder,虽然在顺序流中这通常不是必需的,但在并行流中这是必要的。

请注意,对于上述的特定用例(连接字符串),使用 Collectors.joining() 会更加简洁和高效。但是,这个示例旨在展示如何使用 collect 方法的三个参数版本来自定义收集过程。

3.1 toList/toMap/toSet/toArray()

toList(), toMap(), 和 toSet() 是非常有用的终端操作,它们可以将流中的元素收集到相应的集合中

List<Employee> employees = Arrays.asList(  new Employee("Alice", 5000.0),  new Employee("Bob", 6000.0),  new Employee("Charlie", 5500.0),  new Employee("David", 6500.0)  );  // toList 使用示例  List<String> employeeNames = employees.stream()  .map(Employee::getName)  .collect(Collectors.toList());  System.out.println("Employee Names (toList): " + employeeNames);  // toSet 使用示例  Set<Double> uniqueSalaries = employees.stream()  .map(Employee::getSalary)  .collect(Collectors.toSet());  System.out.println("Unique Salaries (toSet): " + uniqueSalaries);  // toMap 使用示例  // 注意:如果有重名的员工,toMap 会抛出 IllegalStateException,  // 这里假设员工名字是唯一的。如果不唯一,需要处理冲突或选择其他键。  Map<String, Double> employeeSalaries = employees.stream()  .collect(Collectors.toMap(  Employee::getName,  Employee::getSalary  ));  System.out.println("Employee Salaries (toMap): " + employeeSalaries);  // 使用 Stream API 将员工列表转换为 Employee[] 数组  Person[] employeeArray = employees.stream()  .toArray(Employee[]::new); }  

这个例子中,我们首先创建了一个Employee对象的列表。然后,我们使用stream方法将其转换为一个流,并使用map方法来提取员工的名字和薪水。最后,我们使用collect方法和相应的收集器(toList(), toSet(), toMap())来将流中的元素收集到列表、集合或映射中。

3.3 summing/averaging/summarizing

Collectors 类提供了几个用于数据统计的收集器,如 averagingDouble、summarizingDouble 和 summingDouble。这些收集器通常与流的 collect 方法一起使用,用于对数值流(如员工薪水)进行统计。
Collectors提供了一系列用于数据统计的静态方法:
计数:count
平均值:averagingInt、averagingLong、averagingDouble
最值:maxBy、minBy
求和:summingInt、summingLong、summingDouble
统计以上所有:summarizingInt、summarizingLong、summarizingDouble

List<Employee> employees = Arrays.asList(  new Employee("Alice", 5000.0),  new Employee("Bob", 6000.0),  new Employee("Charlie", 5500.0),  new Employee("David", 6500.0)  );  // 使用 averagingDouble 计算平均薪水  double averageSalary = employees.stream()  .collect(Collectors.averagingDouble(Employee::getSalary));  System.out.println("Average Salary (averagingDouble): " + averageSalary);  // 使用 summarizingDouble 获取薪水的统计信息  DoubleSummaryStatistics salaryStats = employees.stream()  .collect(Collectors.summarizingDouble(Employee::getSalary));  System.out.println("Salary Statistics (summarizingDouble): " + salaryStats);  // 使用 summingDouble 计算薪水总和  double sumSalary = employees.stream()  .collect(Collectors.summingDouble(Employee::getSalary));  System.out.println("Sum of Salaries (summingDouble): " + sumSalary);  }  

输出结果:

Average Salary (averagingDouble): 5750.0  
Salary Statistics (summarizingDouble): DoubleSummaryStatistics{count=4, sum=23000.000000, min=5000.000000, average=5750.000000, max=6500.000000}  
Sum of Salaries (summingDouble): 23000.0

在这个例子中:

averagingDouble 方法用于计算员工薪水的平均值。
summarizingDouble 方法返回一个 DoubleSummaryStatistics 对象,该对象聚合了流中所有元素的统计信息,包括计数、总和、最小值、平均值和最大值。
summingDouble 方法用于计算员工薪水的总和。
这些方法都接受一个 ToDoubleFunction 作为参数,这里我们使用 Employee::getSalary 方法引用来提取员工的薪水。收集器会处理流中的元素,并返回相应的统计结果。

3.4 summaryStatistics()

对于数值流(如 IntStream, LongStream, DoubleStream),summaryStatistics返回描述该流统计信息的对象,如最小值、最大值、平均值等。

// 创建一个包含一些双精度浮点数的数组  
double[] numbers = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};  // 使用 DoubleStream 的 of 方法创建一个流,然后调用 summaryStatistics 方法  DoubleSummaryStatistics stats = Arrays.stream(numbers).summaryStatistics();  // 输出统计信息  
System.out.println("最小值: " + stats.getMin());  
System.out.println("最大值: " + stats.getMax());  
System.out.println("平均值: " + stats.getAverage());  
System.out.println("元素数量: " + stats.getCount());  
System.out.println("元素总和: " + stats.getSum());  
3.6 接合joining

Collectors.joining 是一个非常有用的收集器,它可以将流中的元素连接成一个字符串。这对于将列表、集合或其他流数据结构转换为单个字符串表示形式特别有用。

List<Employee> employees = Arrays.asList(  new Employee("Alice", 5000.0),  new Employee("Bob", 6000.0),  new Employee("Charlie", 5500.0),  new Employee("David", 6500.0)  );  // 使用 joining 将员工名字连接成一个字符串,逗号分隔  String namesJoined = employees.stream()  .map(Employee::getName) // 提取员工的名字  .collect(Collectors.joining(", ")); // 使用逗号和空格作为分隔符连接名字  System.out.println("Employee names joined: " + namesJoined);

输出结果:

Employee names joined: Alice, Bob, Charlie, David

在这个例子中,我们调用 Collectors.joining 方法,使用逗号和空格作为分隔符,将名字连接成一个字符串。最后,我们打印出连接后的字符串。

3.7 分组(partitioningBy/groupingBy)

Collectors.joining 是一个非常有用的收集器,它可以将流中的元素连接成一个字符串。这对于将列表、集合或其他流数据结构转换为单个字符串表示形式特别有用。

List<Employee> employees = Arrays.asList(  new Employee("Alice", 5000.0, "Development"),  new Employee("Bob", 6000.0, "Management"),  new Employee("Charlie", 5500.0, "Development"),  new Employee("David", 6500.0, "Management"),  new Employee("Eve", 5200.0, "HR")  );  // 使用 partitioningBy 根据薪水是否高于6000进行分区  Predicate<Employee> salaryAbove6000 = employee -> employee.getSalary() > 6000;  Map<Boolean, List<Employee>> partitionedBySalary = employees.stream()  .collect(Collectors.partitioningBy(salaryAbove6000));  System.out.println("Employees partitioned by salary > 6000: " + partitionedBySalary);  // 使用 groupingBy 根据部门进行分组  Map<String, List<Employee>> groupedByDepartment = employees.stream()  .collect(Collectors.groupingBy(Employee::getDepartment));  System.out.println("Employees grouped by department: " + groupedByDepartment);  }  

请注意,我在 Employee 类中添加了一个 department 字段,在这个例子中:

  • Collectors.partitioningBy 方法用于根据提供的谓词(Predicate)对流中的元素进行分区。在这个例子中,谓词是检查员工的薪水是否高于6000。结果是一个映射,其中键是布尔值(对于满足条件的元素是 true,否则是 false),值是对应分区的员工列表。
  • Collectors.groupingBy 方法用于根据提供的分类函数对流中的元素进行分组。在这个例子中,分类函数是 Employee::getDepartment,它根据员工的部门对员工进行分组。结果是一个映射,其中键是部门名称,值是对应部门的员工列表。

4 其他操作:sequential(顺序流)/parallel(并行流)

parallel和sequential是用来指定流的执行模式的方法。这两种模式决定了流中的元素是如何被处理的。

4.1parallel(并行流,基于ForkJoinPool)

当你调用parallelStream()或者对一个已经存在的流调用parallel()时,你告诉Java这个流应该以并行方式执行操作。并行流会尝试利用多个线程来同时处理多个元素,以提高处理速度。并行流是基于Java的ForkJoinPool实现的,它是一个特殊的线程池,适合执行可以并行处理的任务。

4.2sequential(顺序流)

当你对一个流调用sequential()时,你告诉Java这个流应该以顺序方式执行操作。顺序流中的元素按照它们在数据源中出现的顺序逐个进行处理。顺序流是在单个线程中执行的,因此不存在线程安全问题。

下面是一个简单的示例,演示了如何使用并行流和顺序流,并展示了它们的结果可能有所不同(特别是在并行流中,元素的处理顺序是不确定的)。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);  // 使用顺序流  
System.out.println("Sequential Stream:");  
numbers.stream()  .sequential() // 默认就是顺序流,这里显式指定  .forEach(n -> {  System.out.print(n + " ");  try {  Thread.sleep(100); // 模拟耗时操作  } catch (InterruptedException e) {  e.printStackTrace();  }  });  
System.out.println("\n");  // 使用并行流  
System.out.println("Parallel Stream:");  
numbers.stream()  .parallel()  .forEach(n -> {  System.out.print(n + " ");  try {  Thread.sleep(100); // 模拟耗时操作  } catch (InterruptedException e) {  e.printStackTrace();  }  });  
System.out.println("\n");  

由于并行流中的元素处理顺序是不确定的,每次运行的结果可能会有所不同。但是,一般来说,顺序流会按照元素在数据源中的顺序输出,而并行流则会以任意顺序输出。

顺序流的可能输出(每次运行都应该相同):

Sequential Stream:  
1 2 3 4 5 6 7 8 9 10

并行流的可能输出(每次运行都可能不同):

Parallel Stream:  
4 2 6 8 1 3 9 5 7 10

请注意,在实际的生产代码中,你通常不应该在forEach操作中执行有副作用的操作(比如修改共享变量),因为并行流不保证操作的顺序性。如果你需要收集结果或者执行有状态的操作,应该使用像collect这样的终端操作来代替。

总之,Java8 Stream流提供了一种简洁、高效且易于维护的方式来处理集合元素。通过链式调用一系列的操作,你可以轻松地实现复杂的数据转换和处理逻辑。在实际应用中,你可以根据需要选择合适的中间操作和终端操作,以达到期望的效果。

这篇关于Java8 Stream深度解析:30个案例3万字助你精通集合筛选、归约、分组与聚合操作的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/654769

相关文章

C# WinForms存储过程操作数据库的实例讲解

《C#WinForms存储过程操作数据库的实例讲解》:本文主要介绍C#WinForms存储过程操作数据库的实例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、存储过程基础二、C# 调用流程1. 数据库连接配置2. 执行存储过程(增删改)3. 查询数据三、事务处

Java中StopWatch的使用示例详解

《Java中StopWatch的使用示例详解》stopWatch是org.springframework.util包下的一个工具类,使用它可直观的输出代码执行耗时,以及执行时间百分比,这篇文章主要介绍... 目录stopWatch 是org.springframework.util 包下的一个工具类,使用它

Java进行文件格式校验的方案详解

《Java进行文件格式校验的方案详解》这篇文章主要为大家详细介绍了Java中进行文件格式校验的相关方案,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、背景异常现象原因排查用户的无心之过二、解决方案Magandroidic Number判断主流检测库对比Tika的使用区分zip

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

Java使用Curator进行ZooKeeper操作的详细教程

《Java使用Curator进行ZooKeeper操作的详细教程》ApacheCurator是一个基于ZooKeeper的Java客户端库,它极大地简化了使用ZooKeeper的开发工作,在分布式系统... 目录1、简述2、核心功能2.1 CuratorFramework2.2 Recipes3、示例实践3

Springboot处理跨域的实现方式(附Demo)

《Springboot处理跨域的实现方式(附Demo)》:本文主要介绍Springboot处理跨域的实现方式(附Demo),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录Springboot处理跨域的方式1. 基本知识2. @CrossOrigin3. 全局跨域设置4.

springboot security使用jwt认证方式

《springbootsecurity使用jwt认证方式》:本文主要介绍springbootsecurity使用jwt认证方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录前言代码示例依赖定义mapper定义用户信息的实体beansecurity相关的类提供登录接口测试提供一

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

基于SpringBoot实现文件秒传功能

《基于SpringBoot实现文件秒传功能》在开发Web应用时,文件上传是一个常见需求,然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余,此时可以使用文件秒传技术通过识别重复... 目录前言文件秒传原理代码实现1. 创建项目基础结构2. 创建上传存储代码3. 创建Result类4.

Java利用JSONPath操作JSON数据的技术指南

《Java利用JSONPath操作JSON数据的技术指南》JSONPath是一种强大的工具,用于查询和操作JSON数据,类似于SQL的语法,它为处理复杂的JSON数据结构提供了简单且高效... 目录1、简述2、什么是 jsONPath?3、Java 示例3.1 基本查询3.2 过滤查询3.3 递归搜索3.4