Java8 Stream API之使用流

Java8 Stream API之使用流

Symon 830 2020-11-18

Stream API支持许多操作,这些操作能帮助我们快速完成复杂的数据查询,如筛选、切片、映射、查找、匹配和归约。

筛选和切片

用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。

用谓词筛选 filter

流支持filter方法,该方法接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。例如,下面的代码就是筛选是团员的学生:

List<Student> memberList = students.stream()
        .filter(Student::isMember) //方法引用检查学生是否是团员
        .collect(Collectors.toList());

筛选各异的元素 distinct

流支持distinct方法,该方法返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。例如下面的代码事筛选列表中的偶数,并去重:

List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 2, 4, 6);
numberList.stream()
        .filter(n -> n % 2 == 0)
        .distinct()
        .forEach(System.out::println);

截短流 limit

流支持limit(n)方法,该方法返回一个不超过给定长度的流。如果是有序的流,则最多返回前n个元素。例如,筛选出小于14岁的前三个学生:

List<Student> studentList = students.stream()
        .filter(student -> student.getAge() < 14)
        .limit(3)
        .collect(Collectors.toList());

跳过元素 skip

流支持skip(n)方法,该方法返回一个跳过了前n个元素的流。如果流中的元素不足n个,则返回一个空流。例如,跳过年龄小于14岁的头三个学生,返回剩下的。

List<Student> studentList = students.stream()
        .filter(student -> student.getAge() < 14)
        .skip(3)
        .collect(Collectors.toList());

映射

有时候当我们在处理数据时,需要从一系列对象中提取某个属性值,比如从SQL表中选择一列。Stream API通过map和flatMap方法提供了类似的工具。

对流中的每一个元素应用函数 map

流支持map方法,该方法接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。例如,需要提取学生列表中的学生姓名:

List<String> studentList = students.stream()
        .map(Student::getName)//getName会返回String,此时map返回的就是Stream<String>
        .collect(Collectors.toList());

如果需要进一步操作,例如获取姓名的长度,在链接上一个map即可:

List<Integer> studentList = students.stream()
        .map(Student::getName)
        .map(String::length)
        .collect(Collectors.toList());

流的扁平化 flatMap

先举个例子,我们需要从["hello", "world"]单词列表中提取每个字符并去重,结果应该是["h", "e", "l", "o", "w", "r", "d"]

我们可能会写出下面这样的代码:

wordList.stream()
        .map(word -> word.split(""))
        .distinct()
        .collect(Collectors.toList());

但实际上map返回的是Stream<String[]>,或者我们又写出下面这样的代码:

wordList.stream()
        .map(word -> word.split(""))
        .map(Arrays::stream)
        .distinct()
        .collect(Collectors.toList());

但实际上map返回的是Stream<Stream<String>>,而我们真想想要的是Stream<String>

这时候flatMap就派上用场了:

wordList.stream()
        .map(word -> word.split(""))
        .flatMap(Arrays::stream)
        .distinct()
        .collect(Collectors.toList());

map(Arrays::stream)是将每个元素都映射成一个流,而flatMap方法效果将映射的流合并起来,即扁平化一个流。意思就是将流中的每个值都转换成一个流,然后把所有的流连接起来成为一个流

查找和匹配

还有在处理数据时,我们会判断一个集合中的某些元素是否符合给定的条件。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。

检查谓词是否至少匹配一个元素 anyMatch

anyMatch方法可以用来判断“流中是否有一个元素能匹配给定的谓词”。

if(students.stream().anyMatch(Student::isMember)) {
    System.out.println("学生列表中至少有一个是团员!");
}

anyMatch方法返回一个boolean,是一个终端操作。

检查谓词是否匹配所有元素 allMatch noneMatch

allMatch方法可以用来判断“流中是否所有的元素都能匹配给定的谓词”

if (students.stream().allMatch(student -> student.getAge() < 18)) {
    System.out.println("学生列表中所有学生的年龄都小于18岁!");
}

noneMatch和allMatch是相对的,用来判断“流中的所有元素都不能匹配给定的谓词”

if (students.stream().noneMatch(student -> student.getAge() >= 18)) {
    System.out.println("学生列表中没有学生的年龄大于等于18岁!");
}

anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是我们熟悉的Java中的&&和||运算符短路在流中的版本。

查找元素 findAny

findAny方法返回当前流中的任意元素。可与其他流操作相结合使用。例如,我们需要找到一个学生列表中的团员:

 Optional<Student> studentOptional = students.stream()
        .filter(Student::isMember)
        .findAny();

不过这个Optional是什么?
Optional简介
Optional<T> 类(java.util.Optional)是一个容器类,代表一个值存在或不存在。上面代码中有可能什么元素都没有找到。Java 8引入 Optional<T> 用来避免null带来的异常。先简单了解下它的几个方法:

  • isPresent():若Optional包含值则返回true,否则返回false。
  • ifPresent(Consumer<? super T> consumer):若Optional包含值时执行给定的代码。参数是Consumer,一个函数式接口。
  • T get():若Optional包含值时返回该值,否则抛出NoSuchElementException异常。
  • T orElse(T other):若Optional包含值时返回该值,否则返回指定默认值。

例如,前面的studentOptional若包含一个学生,我们就打印该学生的姓名,否则就什么也不做。

studentOptional.ifPresent(student -> System.out.println(student.getName()));

查找第一个元素 findFirst

findFirst方法返回当前流中的第一个元素。在某些顺序流中,我们要找到第一个元素,这时可以使用findFirst,例如,给定一个数字列表,找到其中第一个平方根能被2整除的数:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Optional<Integer> first = numbers.stream()
        .map(n -> n * n)
        .filter(n -> n % 2 == 0)
        .findFirst();
first.ifPresent(n -> System.out.println(n));// 4

我们会发现findAny和findFirst的工作方式是类似的,那我们什么时候使用findFirst和findAny呢?
findFirst在并行上限制很多,所以如果不关心返回元素是哪一个(不关心顺序),请使用findAny,因为在并行时限制较少。

归约 reduce

reduce操作可以将一个流中的元素组合起来,得到一个值。

元素求和

使用foreach循环求和:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sum = 0;
for (Integer number : numbers) {
    sum += number;
}

这里通过反复加法,把一个列表归约成一个数字。如果是计算相乘的结果,是不是还要复制粘贴这段代码?大可不必,reduce操作将这种模式做了抽象。用reduce求和:

Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);

这里reduce()接受两个参数:

  • 初始值,这里是0
  • BinaryOperator组合两个元素产生新值,这里是Lambda(a, b) -> a + b
    如果是相乘,则只需传递Lambda(a, b) -> a * b即可:
Integer result = numbers.stream().reduce(0, (a, b) -> a * b);

Java 8中,Integer类有了static int sum(int a, int b)方法来求两个数的和,这刚好是BinaryOperator类型的值,所以代码可以更精简,更易读:

Integer sum = numbers.stream().reduce(0, Integer::sum);

reduce还有一个重载版本,不接受初始值,返回一个Optional对象,考虑流中没有任何元素,也没有初始值,所以reduce无法返回结果,此时Optional中的值就不存在。

最大值和最小值

类比求和的操作,我们传递Lambda(a, b) -> a > b ? a : b即可:

numbers.stream().reduce((a, b) -> a > b ? a : b);

相同的Java 8中的Integer也新增了max和min来求两个数中的最大和最小值,则可以写成:

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

数值流

前面用reduce计算流中元素的中和,现在我想计算学生列表中学生的年龄总和,就可以这么写:

students.stream().map(Student::getAge)
        .reduce(0, Integer::sum);

但是这里暗含了装箱成本。Integer需要被拆箱成原始类型,在进行求和。所以Stream API提供了原始类型流特化,专门支持处理数流的方法。

原始类型流特化

Java 8中引入了三个原始类型特化流:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免暗含装箱成本。每个接口都带来了进行常用数值归约的新方法,如对数值流求和的sum,找到最大元素的max,还可以把它们再转回对象流。

映射到数值流 mapToInt mapToDouble mapToLong

students.stream()
        .mapToInt(Student::getAge)
        .sum();

转回对象流 .boxed()

Stream<Integer> stream = students.stream()
        .mapToInt(Student::getAge)
        .boxed();

默认值 OptionalInt OptionalDouble OptionalLong

OptionalInt optionalInt = students.stream()
        .mapToInt(Student::getAge)
        .max();

数值范围 range rangeClosed

Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助我们生成一个数值范围。range和rangeClosed,两个方法接收两个参数第一个是起始值,第二个是结束值。前者不包含结束值,后者包含结束值。

IntStream range = IntStream.range(1, 100);
System.out.println(range.count());//99
IntStream intStream = IntStream.rangeClosed(1, 100);
System.out.println(intStream.count());//100

数值流例子

勾股数:a*a + b*b = c*c,a、b、c都是整数。

Stream<int[]> stream = IntStream.rangeClosed(1, 100)
        .boxed()
        .flatMap(a ->
                IntStream.rangeClosed(a, 100)
                        .mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)})
                        .filter(ints -> ints[2] % 1 == 0)
        );

stream.limit(10)
        .forEach(t -> System.out.println(t[0] + "--" + t[1] + "--" + t[2]));

构建流

前面我们已经了解了很多流的的操作,并且知道通过stream方法从集合生成流以及根据数值范围创建数值流。下面我们我们介绍如何从序列、数组、文件和生成函数来创建流。

由值创建流 Stream.of

使用静态方法Stream.of显示的创建一个流,该方法接受任意数量的参数。

//创建一个字符串流
Stream<String> stringStream = Stream.of("A", "B", "C", "D", "E");
stringStream.forEach(System.out::println);
//创建一个空流
Stream.empty();

由数组创建流 Arrays.stream

使用静态方法Arrays.stream从数组创建一个流,该方法接受一个数组参数。

int[] num = {1,2,3,4,5};
IntStream stream = Arrays.stream(num);
System.out.println(stream.sum());//15

由文件生成流

Java 8更新了NIO API(非阻塞 I/O),其中java.nio.file.Files中新增了许多静态方法可以返回一个流,如Files.lines()该方法接受一个文件路径对象(Path对象),返回由指定文件中每一行组成的字符串流。下面的代码用来计算这个文件中有多少个不同的字符:

try (Stream<String> lines = Files.lines(Paths.get("C:\\Users\\symon\\Desktop\\test.txt"), Charset.defaultCharset())) {
    long count = lines
            .flatMap(line -> {
                System.out.println(line);
                return Arrays.stream(line.split(""));
            })
            .distinct()
            .count();
    System.out.println(count);
} catch (IOException e) {
    e.printStackTrace();
}

由函数生成流,创建无限流

Stream API提供了两个静态方法来生成流:Stream.iterate()Stream.generate()。这两个操作可以创建所谓的无限流:没有固定大小的流。一般来说,会使用limit(n)来进行限制,避免无尽地计算下去。

迭代 Stream.iterate
Stream.iterate(0, n -> n + 2)
        .limit(20)
        .forEach(System.out::println);

iterate方法接受一个初始值(这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator类型)。这里是n -> n + 2,返回前一个元素加2。所以上面代码生成了一个正偶数流。如果不加limit限制,则会永远计算下去。

生成 Stream.generate
Stream.generate(Math::random)
        .limit(10)
        .forEach(System.out::println);

generate方法接受一个Supplier类型的Lambda作为参数,不会像iterate一样对每个新生成的值应用函数。上面代码是取10个0~1之间的随机数。同样,如果不加limit限制,该流也会无限长。


# java # java8