Java Stream API使用方法

Java Stream API是Java 8中引入的一个新特性,它允许开发人员用声明式的方式来处理数据,这种方式更加简洁易读,并且可以利用多核架构实现并行操作。
假设有一个列表,其内容为1、2、3,要求把里面每个数字进行平方之后筛选出小于5的数字,以传统写法应该这样写:

public List<Integer> squareAndFilter(List<Integer> nums) {
    List<Integer> resule = new ArrayList<>();
    for (int i = 0; i < nums.length; i++) {
      int square == nums[i] * nums [i];
      if (square < 5) {
        result.add(square);
      }
    }
    return result;
}

这样的写法看起来非常不直观,而且如果需求进一步复杂,代码的可读性就会更低。但是如果使用Stream API,上述功能就可以这样写:

public List<Integer> squareAndFilter(List<Integer> nums) {
    return nums.stream()  // 构造一个Stream
        .map(n -> n * n)  // 将里面的元素平方
        .filter(n -> n < 5)  // 找出小于5的元素
        .collect(Collectors.toList())  // 将其中所有元素收集到一个List中
}

可以看到,使用Stream API之后,编写的代码在可读性方面有了很大的提升。
Stream API相对于传统的for循环的命令式写法具有以下的优点:

  1. 声明式编程:Stream API使用声明式编程的模式,开发人员可以描述想要的结果,而不是具体的实现步骤,让代码变得更加简洁易读。
  2. 链式操作:Stream API的所有中间步骤都返回一个新的Stream,可以在此基础上进行后续的操作,使代码更加紧凑。
  3. 并行处理:Stream API支持并行处理,可以利用多核处理器进行并行操作,提高代码性能。
  4. 更加灵活:Stream API提供了很多高级操作,比如map、reduce、filter等,让开发人员很容易地对集合进行复杂的操作。
  5. 性能优化:Stream API在内部进行了许多操作,比如延迟执行(只在用到数据的时候开始求值)、短路操作(一旦有结果就停止运算)等,进一步提高了代码的性能。

构造一个Stream

Java提供了多种构造Stream的方式,以下进行简单列举:

  1. 从Collection构造:Java的常用数据结构比如ListSet等,它们都继承了java.utll.Collection接口,这个接口提供了一个stream()方法,可以快速地根据集合内的元素生成一个Stream。Map虽然没有直接实现Collection接口,但是可以通过它的keySet()values()entrySet()方法来间接构造一个Stream。示例代码如下:
// List、Set同理
List<String> list = Arrays.asList("a", "b", "c");  
Stream<String> stream = list.stream();

Map<String, Integer> map = new HashMap<>();  
map.put("a", 1);  
map.put("b", 2);  
map.put("c", 3);  
  
Stream<String> keyStream = map.keySet().stream();
Stream<Integer> valueStream = map.values().stream();  
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
  1. Stream.of:这是一个静态方法,可以传递任意数量的参数(包括数组),并返回Stream:
Stream<String> stream = Stream.of("a", "b", "c")
  1. Arrays.stream:同样是一个静态方法,可以接受一个数组,然后返回对应的Stream:
int[] arr = {1, 2, 3, 4, 5};
Stream<Integer> stream = Arrays.stream(arr);
  1. Stream.generate或Stream.iterate:这两个静态方法可以生成无线长度的Stream,前者通过Supplier生成,后者通过初始值和一个UnaryOperator生成,可以配合limit方法限制Stream的长度:
Stream<String> generatedStream = Stream.generate(() -> "element").limit(10);  
Stream<Integer> iteratedStream = Stream.iterate(0, n -> n + 2).limit(10);
  1. 从文件构造:java.nio.file.Files类的lines()方法可以读取文件,然后将文件中的每一行组合成一个Stream:
Path path = new Path("file.txt");
Stream<String> stream = Files.lines(path);

特定的Stream类型

最常用的Stream类型是Stream<T>,表示任意类型的Stream。除此之外,Java还提供了IntStreamLongStreamDoubleStream,表示对特定数字类型的Stream的特化实现。

Stream API各种方法

查看文章开头示例的代码:

public List<Integer> squareAndFilter(List<Integer> nums) {
    return nums.stream()  // 构造一个Stream
        .map(n -> n * n)  // 将里面的元素平方
        .filter(n -> n < 5)  // 找出小于5的元素
        .collect(Collectors.toList())  // 将其中所有元素收集到一个List中
}

这个功能的实现分成了三个部分:构造Stream、对Stream进行各种操作、收集最终结果。其中构造Stream的部分前面已经讲过了,中间的mapfilter方法表示对Stream进行的各种操作,最后的collect方法用于收集Stream的元素,得到最终的结果。中间操作不会被实际执行,只是在Stream中添加一个操作的记录,只有在最后收集结果的时候才会真正执行,这也就是Stream能够延迟执行的根本原因。接下来我们将对后两者进行讲解。

Stream API的中间操作

Filter和Map

Stream API的filter方法用于过滤Stream中的元素。它需要传入一个Predicate,用于筛选出符合条件的元素。示例代码如下,它的最终结果是{3, 4}

List<Integer> original = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = original
                        .stream()
                        .filter(n -> n > 2)
                        .filter(n -> n < 5)
                        .collect(Collectors.toList());

除了filter之外,Stream另一个非常重要的API就是map,他可以将数据从一种形式转换成另一种格式。示例代码如下,它的结果是{2, 5, 10, 16, 26}

List<Integer> original = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = original
                        .stream()
                        .map(n -> n * n)
                        .filter(n -> n + 1)
                        .collect(Collectors.toList());

对于前面提高的Stream的特化比如IntStream等,Stream也提供了对应的方法,分别是mapToIntmapToLongmapToDouble,他们接受的参数要求输入是泛型参数T,输出类型是对应的Integer、Long和Double。

去重、排序

Stream API的distinctsorted方法分别提供了对Stream中的元素进行去重和排序的功能。如果没有这两个方法,那么去重只能通过创建集合再遍历添加的方式实现,比较繁琐。而且排序的功能也可以和后续的方法结合,能够让Stream提前返回,提高操作性能。
Distinct操作的示例代码如下,它的结果为{1, 2, 3}

List<Integer> original = Arrays.asList(1, 2, 3, 3, 3);
List<Integer> result = original
                        .stream()
                        .distinct()
                        .collect(Collectors.toList());

Sorted操作的示例代码如下,它的结果为{1, 2, 3}

List<Integer> original = Arrays.asList(3, 2, 1);
List<Integer> result = original
                        .stream()
                        .sorted()
                        .collect(Collectors.toList());

同时,Sorted方法也可以传入一个Comparator,用于自定义排序的方式以及依据的属性。

限制和跳过

在前面介绍的构造Stream的那一小节中,我们介绍了构造一个无线长度的Stream的方法。在那个方法中,我们使用了limit方法来限制Stream中的元素个数,只使用其中的前n个元素。Stream的另一个方法skip则可以跳过前n个元素,选择后面的所有元素。二者的示例代码如下,他们的输出分别是{1, 2, 3}{4, 5}

List<Integer> original = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> resultLimit = original
                        .stream()
                        .limit(3)
                        .collect(Collectors.toList());

List<Integer> resultSkip = original
                        .stream()
                        .skip(3)
                        .collect(Collectors.toList());                    

选择和删除

[!warning] takeWhiledropWhile两个方法和filter方法并不等价!

前面介绍的Limit和Skip,它们都会无条件地选择或者放弃元素,但是现实中有很多的条件是需要进行,这时就会用到takeWhiledropWhile方法:

List<Integer> original = Arrays.asList(2, 1, 5, 4, 3);
// 结果为{2, 1}
List<Integer> resultLimit = original
                        .stream()
                        .takeWhile(n -> n < 5)
                        .collect(Collectors.toList());

// 结果为{5, 4, 3}
List<Integer> resultSkip = original
                        .stream()
                        .dropWhile(n -> n < 4)
                        .collect(Collectors.toList());    

需要注意的是,这两个方法和Filter并不等价,就像上面的代码一样,虽然后面仍然有符合条件的元素,但是Stream在前面就已经满足条件提前返回了,因此后面再有不符合条件的元素也会被一并保留。

各种FlatMap方法

考虑如下需求,有两个字符串"Hello"和"World",将其合并为一个字符列表。很多人第一感觉是使用Stream,对两个字符串调用split方法,得到对应的字符数组,然后再收集到一个List中,代码如下:

String[] words = ["Hello", "World"];

Arrays.stream(words).map(s -> s.split("")).collect(Collectors.toList())

但是这样返回的类型是List<String[]>,而不是List<String>。这是因为在第一步的Map中,每个字符串在分割之后的类型是String[],然后调用collect方法的时候是将数组本身组成一个列表。
如果需要实现题目的需求,那么这时就需要使用flatMap方法了:

String[] words = ["Hello", "World"];
List<String> result = Arrays.stream(words)
                            .map(s -> s.split(""))
                            .flatMap(Arrays::stream)
                            .collect(Collectors.toList())

使用flatMap方法可以将各个数组(流)映射为流的内容,并将产生的多个流被合并在一起,生成一个新的流。flatMap方法要求函数的输入是T,输出为一个新的Stream,而且新的Stream类型不必和T一致。
对于IntStream等特化的Stream,Java也提供了对应的方法比如flatMapToInt等。

执行额外操作(Peak)

这个操作不太常用,主要用作调试Stream时使用:

List<Integer> original = Arrays.asList(1, 2, 3);

List<Integer> result = original
                        .stream()
                        .peak(n -> System.out::print)
                        .collect(Collectors.toList());

执行时会打印:

1
2
3

Stream API的最终操作

前面介绍的中间操作的返回值都是一个新的Stream,允许开发人员在结果上做出进一步的操作。最终操作返回的结果类型不是Stream,可能是转换为List、Array或者是一个副作用比如遍历所有的元素等。调用最终操作时,Stream被消费,而且不能再被使用。

收集其中的元素和ToArray

Stream API的ToArray操作用于将Stream中的元素收集到一个集合或者是数组中,示例代码如下:

List<Integer> original = Arrays.asList(1, 2, 3);
Integer[] result = original.stream().toArray();

像前面演示的代码一样,Stream API的collect方法,同样能够将元素收集起来,常用的几个方法是toListtoSetgroupBy,其中toListtoSet用于将Stream中的元素转换为List或Set,groupBy方法用于将Stream中的元素分类。考虑一下对象结构:

@Data
class StudentGrade {
    private String name;  // 学生姓名
    private Integer grade;  // 分数
}

如果要根据学生姓名查看他的所有成绩,那么可以使用以下方式:

List<StudentGrade> original = new ArrayList();  // 假装这里已经初始化了一些数据
Map<String, List<StudentGrade>> result = original
                                    .stream()
                                    .collect(Collectors.groupBy(StudentGrade::getName));

这样就能够根据学生姓名进行分组操作。
Java的Collectors还提供了多种操作,用于收集数据,包括最大最小值以及拼接为字符串等,这些我们放到后面进行讲解。

ForEach

在处理完Stream后,如果需要对其中的元素直接进行处理,可以使用forEachforEachOrdered方法,这里以forEach方法为例:

List<Integer> original = Arrays.asList(1, 2, 3);

original
    .stream()
    .map(n -> n * n)
    .forRach(n -> System.out::print);

上面代码的输出是:

1
4
9

使用forEachOrdered方法可以保证处理元素的顺序和它们在Stream中的顺序是一致的,这个在并行流(ParallelStream)中会比较有用。如果Stream本身不是并行流,那么这个方法的执行顺序和forEach方法一致。

Min、Max

如果Stream中的元素可以比较,比如是Integer、String等类型或者实现了Comparator接口,那么可以使用minmax方法来获取Stream中的最值,这里以最小值为例:

List<Integer> original = Arrays.asList(1, 2, 3);
Optional<Integer> result = original
                            .stream()
                            .min(Comparator.comparing(Integer::intValue));

List<String> originalStr = Arrays.asList("1", "2", "3");
Optional<String> result = originalStr
                            .stream()
                            .min(Comparator.comparing(String::valueOf));

这里的返回值是Optional<T>类型,如果Stream本身为空,则返回一个空的Optional。如果选择出来的最小值是null,那么会产生一个NPE。

Count

使用Count方法可以统计Stream中的元素个数:

List<Integer> original = Arrays.asList(1, 2, 3);
long count = original.stream().count();  // 3

Match

有些时候,在对Stream中的元素处理之后,我们希望看一下其中的元素是否满足条件,这时可以使用anyMatchallMatchnoneMatch方法实现,他们都需要一个Predicate作为参数,然后返回一个boolean类型的结果,这里以anyMatch为例:

List<Integer> original = Arrays.asList(1, 2, 3);
boolean anyMatch = original.stream().anyMatch(n -> n < 2);  // true

这三个方法的含义如下:

  1. anyMatch:Stream中至少有一个元素满足要求。
  2. allMatch:Stream中的每个元素都满足需求。
  3. noneMatch:Stream中的每个元素都不满足需求。

Reduce

有些时候我们需要对序列中的元素进行累积操作,得到最终的结果,比如根据某个字段求和,这样的操作就叫做Reduce,而前面提到的Min、Max和Count也是一种特殊的Reduce操作。我们以求和操作为例:

List<Integer> original = Arrays.asList(1, 2, 3);
Optional<Integer> sum = original.stream()
                            .reduce((x, y) -> x += y);  // 结果为6

这是reduce方法的第一个重载,接收一个BinaryOperator作为参数(叫做Accumulator)。一个BinaryOperator的函数参数类似于T apply(T, T),第一个参数是上次执行后的返回值,第二个参数是Stream中的元素。在第一次执行时,第一个参数是Stream中前两个元素进行运算(这里是相加)的结果。如果Stream本身为空,则返回的Optional也为空。
Reduce方法的第二个重载允许传入一个初始值,就像这样:

List<Integer> original = Arrays.asList(1, 2, 3);
Optional<Integer> sum = original.stream()
                            .reduce(1, (x, y) -> x += y);  // 结果为7

[!warning] 这段代码在并行Stream中会与上述结果不一致,请不要这样写!

根据Java文档,Identity的参数是对于Accumulator的标识,它要求对于任意的t,accumulator.apply(t)的结果都是t。因为在并行Stream中,每个线程都会创建一个Accumulator,如果Identity不满足上述条件,那么在执行时Identity就会被多次初始化。以上面的代码为例,如果开启了3个线程,那么Identity就会被累加三次,最终结果是9,与单线程状态下的结果(7)不一致。
Reduce的第三个重载同样是为了在并行Stream中使用,它的函数声明为:

<U> U reduce(U identity,  
             BiFunction<U, ? super T, U> accumulator,  
             BinaryOperator<U> combiner);

其中U是最终结果的类型,T是Stream中元素的类型。在执行时,并行Stream开启多个线程处理,每个Accumulator根据Identity提供的初始值进行计算,并在计算之后将当前部分的结果返回给Combiner,Combiner遍历所有的返回结果,从而得到最终结果。
另外:为了保证Stream在并行执行时能够获得一致的结果,上面所有的Accumulator和Combiner都必须满足结合律,即(a op b) op c == a op (b op c)。

FindFirst和FindAny

这两个方法用于从Stream中获取一个元素,他们的返回值都是Optional<T>,如果Stream为空,则返回的Optional也为空,如果返回的元素是null,那么将产生NPE,他们的函数签名如下:

Optional<T> findFirst();
Optional<T> findAny();

对于findFirst方法,如果对应的Stream有顺序,则会返回它的第一个元素,findAny方法则返回Stream中的任意一个元素。
关于元素的顺序:某些Stream中的元素是有一定顺序的,比如数组就是它的元素排列顺序;某些集合,比如HashSet,它的元素没有特定顺序,因此它对应的Stream也就没有顺序。

Java 16引入的新API

目前随着Spring Boot 3的发行,其要求的Jdk最低版本也升级到了17。而在上一个版本Java 16中,针对Stream API有两个更新,分别是toList方法和mapMulti方法。

ToList

在前面的代码中,如果要将Stream中的元素收集到一个List中,需要调用collect(Collectors.toList())方法,这样写起来比较麻烦,因此Stream API引入了toList方法,可以简化代码编写:

List<Integer> original = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = original
                        .stream()
                        .toList();

而且与使用Collector的方法相比,因为不涉及Collector的截断操作,因此相比collect方法能够带来一定的性能提升。
需要注意的是,toList方法返回的是不可变列表,如果需要修改结果的话,还要使用collect方法返回一个可变列表,在这种情境下二者不能互相替换。

MapMulti

[!info] MapMulti方法没有限制泛型参数,需要手动指定,否则返回类型是Object。

以前面提到的获取字符数组为例:

String[] words = ["Hello", "World"];
List<String> result = Arrays.stream(words)
                            .map(s -> s.split(""))
                            .flatMap(Arrays::stream)
                            .collect(Collectors.toList())

上述代码有一个缺点,就是当words特别长时,调用flatMap方法将会产生很多的Stream,这会造成额外的性能开销,这时候就可以考虑使用mapMulti方法:

String[] words = ["Hello", "World"];
List<String> result = Arrays.stream(words)
                            .map(s -> s.split(""))
                            .mapMulti<String>((str, consumer) -> {
                                for (char c : str.toCharArray()) {  
                                    consumer.accept(c);  
                                }
                            })
                            .collect(Collectors.toList())

MapMulti方法接收的方法叫做Mapper,这个方法接收两个参数,第一个参数是Stream中的当前元素,第二个参数是一个Consumer,用于接收转换后的参数。这里我们将每个str转换成为字符数组,并在之后调用Consumer接收其中的每个字符。查看Stream的内部实现,Mapper内部构造了一个Buffer,并将Consumer接收的元素放入Buffer中,并在迭代之后根据Buffer构造出一个Stream,这样就能起到减少Stream构造开销的作用。

Stream API中的Collector

前面提到的collect方法的参数是一个Collector,用于收集Stream中的数据,现在对其作出进一步的介绍。以下所有的演示代码都使用下面的数据作为数据源:

// 所有字段都一致的时候认为两个对象相等
@Data  
public class Salary {  
    private String company;  
    private String department;  
    private String name;  
    private int age;  
    private int salary;  
    
    public Salary(String company, String department, String name, int age, double salary) {  
        this.company = company;  
        this.department = department;  
        this.name = name;  
        this.age = age;  
        this.salary = salary;  
    }  
}

List<Salary> salaries = Arrays.asList(  
    new Salary("总公司", "行政部", "员工A", 25, 5000),
    new Salary("总公司", "行政部", "员工A", 25, 10000),
    new Salary("总公司", "市场部", "员工B", 35, 15000),
    new Salary("分公司", "市场部", "员工C", 30, 10000),
    new Salary("分公司", "行政部", "员工D", 40, 5000),
    new Salary("分公司", "研发部", "员工E", 55, 6000),
    new Salary("分公司", "研发部", "员工F", 30, 8000),
    new Salary("分公司", "研发部", "员工G", 25, 5000),
    new Salary("分公司", "产品部", "员工H", 30, 6000),
);

收集到集合

Collector最常用的操作就是将Stream中的元素放入一个集合中,除了前面提到的toList方法,Collectors类还提供了将元素放入其他类型的功能,比如Set或者是自定义的Collection。转换成Set的示例代码如下:

Set<Salary> result = original.stream().collect(Collectors.toSet());
System.out.println(result.size());  // 8,因为有两个元素是重复的,只能留下一个

使用toList或者toSet方法时,用户无法指定使用的实现类,如果需要将Stream中的元素收集到自定义容器中,可以使用toCollection方法:

List<Salary> result = original
                        .stream()
                        .collect(Collectors.toCollection(
                            ArrayList::new
                        ));

这个方法的参数是一个Supplier<C extends Collection<T>>,一般情况下传入构造方法即可,也可以传入自定义的方法来返回创建好的容器。
除了这些方法以外,在Java 10中,Collectors还提供了toUnmodifiableListtoUnmodifiableSet方法,用于返回不可变的List和Set。
这里列举一下各个方法的使用场景:

  1. toCollection:需要将元素收集到自定义的容器中使用的时候。
  2. toSettoList:将元素收集到List和Set中,返回的列表和集合可变,即可以添加和删除元素。
  3. toUnmodifiableListtoUnmodifiableSet(Java 10引入):将元素收集到List和Set中,但是返回的集合不可变。

拼接成字符串

使用joining方法可以将Stream中的内容拼接为一个字符串,使用方法如下:

String result = original
                    .stream()
                    .map(s -> s.getName())
                    .collect(Collectors.joining("\n"));

System.out.println(result);

代码的输出结果如下:

员工A
员工A
员工B
员工C
员工D
员工E
员工F
员工G
员工H

joining方法一共有三个重载:

  1. joining():将Stream中的元素拼接在一起(使用空字符串作为分隔符)。
  2. joining(CharSequence delimiter):将元素拼接为字符串,使用delimiter作为分隔符,
  3. joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix):使用delimiter作为分隔符,并在拼接的结果上添加上前缀和后缀。

各种统计方法

前面提到的求最大值,最小值的操作,同样可以通过Collector实现。首先是计数,这个通过counting实现:

long count = original.stream().collect(Collectors.counting());  // 9

求最值,这个可以通过minBymaxBy实现,使用时需要传入一个Comparator:

int minAge = original
                .stream()
                .map(Salary::getAge)
                .collect(Collectors.minBy((s, v) -> s.compareTo(v)));

求和可以通过summingIntsummingLongsummingDouble方法实现,这三个函数分别需要传入一个mapToIntmapToLongmapToDouble方法,将Stream中的元素转换为对应的类型:

int salarySum = original
                    .stream()
                    .filter(s -> s.getDepartment().equals("市场部"))
                    .collect(Collectors.summingInt(s -> s.getSalary()));
// 最后结果为19000

求平均数的方法可以用averagingBy方法,使用时同样需要传入MapToFunction,将元素转换为对应的类型。

分组

对于前面的数据集,如果计算总公司和分公司的人数,那么可以使用groupingBy方法,用于对数据快速进行分组:

Map<String, Integer> result = original
                                .stream()
                                .distiinct()  // 这里进行去重
                                .collect(Collectors.groupingBy(
                                    Salary::getCompany,
                                    Collectors.counting()
                                ));
// 结果:总公司-3;分公司-6

groupingBy方法还有如下的重载:

  1. groupingBy(classifier):对Stream中的元素进行分组,Key是classifier的返回值,Value是调用toList后的元素列表。
  2. groupingBy(classifier, downstream):对元素进行分组,并对Value进行后续处理,比如放入Set、求和等。
  3. groupingBy(classifier, mapFactory, downstream):对元素进行分组,可以自定义Map的实现类,要求的函数类型是Supplier<Map>

除了上面提到的几种,Collectors还提供了reducemapping等方法,如果有需要可以自行查看。

  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OriginCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值