首页IT科技Java 8 Stream API 引入和使用

Java 8 Stream API 引入和使用

时间2025-09-19 13:16:55分类IT科技浏览6806
导读:Java 8 系列文章 持续更新中...

Java 8 系列文章 持续更新中

引入流

流是什么

流是Java API的新成员                ,它允许你以声明性的方式处理数据集合                。可以看成遍历数据集的高级迭代                         。流可以透明地并行处理                         ,无需编写多线程代码          。我们先简单看一下使用流的好处            。下面两段代码都是用来返回年龄小于14岁的初中生的姓名          ,并按照年龄排序                        。

假如我们有下面Student实体类

@Data public class Student { private String name; private int age; private boolean member; private Grade grade; public Student() { } public Student(String name, int age, boolean member, Grade grade) { this.name = name; this.age = age; this.member = member; this.grade = grade; } public enum Grade{ JUNIOR_ONE,JUNIOR_TWO,JUNIOR_THREE } }

Java 8之前的实现方式:

List<Student> students = Arrays.asList( new Student("张初一", 13, false, Student.Grade.JUNIOR_ONE), new Student("李初二", 14, false, Student.Grade.JUNIOR_TWO), new Student("孙初三", 15, false, Student.Grade.JUNIOR_THREE), new Student("王初一", 12, false, Student.Grade.JUNIOR_ONE), new Student("钱初二", 14, false, Student.Grade.JUNIOR_TWO), new Student("周初三", 16, false, Student.Grade.JUNIOR_THREE)); List<Student> resultStudent = new ArrayList<>(); //垃圾变量            ,一次性的中间变量 //foreach循环                        ,根据条件筛选元素 for (Student student : students) { if (student.getAge() < 14) { resultStudent.add(student); } } //匿名类              ,给元素排序 Collections.sort(resultStudent, new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return Integer.compare(o1.getAge(), o2.getAge()); } }); List<String> resultName = new ArrayList<>(); //foreach循环        ,获取元素属性 for (Student student : resultStudent) { resultName.add(student.getName()); }

Java 8流的实现方式:

List<String> resultName = students.stream() .filter(student -> student.getAge() < 14) //年龄筛选 .sorted(Comparator.comparing(Student::getAge)) //年龄排序 .map(Student::getName) //提取姓名 .collect(Collectors.toList());//将提取的姓名保存在List中

为了利用多核架构并行执行这段代码                        ,只需要把stream()替换成parallelStream()即可              。

通过对比两段代码之后                  ,Java8流的方式有几个显而易见的好处        。

代码是以声明性的方式写的:说明想要做什么(筛选小于14岁的学生)而不是去说明怎么去做(循环                、if) 将几个基础操作链接起来    ,表达复杂的数据处理流水线(filter->sorted->map->collect),同时保持代码清晰可读                        。

总结一下                        ,Java 8的Stream API带来的好处:

声明性-更简洁                      ,更易读 可复合-更灵活 可并行-性能更好

流简介

流到底是什么?简单定义:“从支持数据处理操作的源生成的元素序列                ”,下面剖析这个定义                  。

元素序列:像集合一样                    ,流也提供了一个接口                          ,可以访问特定元素类型的一组有序值    。集合讲的是数据     ,流讲的是计算                        。 源:流使用一个提供数据的源                ,如集合                         、数组或输入/输出资源                      。 数据处理操作:流的数据处理功能之处类似于数据库的操作                         ,以及函数式编程语言中的常用操作          ,如filter          、map            、reduce                        、find              、match        、sort等。流的操作可以顺序执行            ,也可以并行执行                    。 流水线:很多流的操作会返回一个流                        ,这样多个操作就可以链接起来              ,形成一个流水线                          。可以看成数据库式查询     。 内部迭代:于迭代器显示迭代的不同        ,流的迭代操作是在背后进行的                。

看一段代码                        ,更好理解这些概念

List<String> resultName = students.stream() //从列表中获取流 .filter(student -> student.getAge() < 16) //操作流水线:筛选 .map(Student::getName) //操作流水线:提取姓名 .limit(2) //操作流水线:只取2个 .collect(Collectors.toList());//将结果保存在List中

在上面代码中                  ,数据源是学生列表(students)    ,用来给流提供一个元素序列                        ,调用stream()获取一个流                      ,接下来就是一系列数据处理操作:filter                        、map                  、limit和collect                         。除collect之外,所有这些操作都会返回一个流                    ,组成了一条流水线          。最后collect操作开始处理流水线                          ,并返回结果            。

流与集合

粗略的说     ,流与集合之间的差异就在于什么时候进行计算                        。

集合是一个内存中的数据结构(可以添加或者删除)                ,它包含数据结构中目前所有的值——集合中的每个元素都是预先处理好然后添加到集合中的              。 流则是在概念上固定的数据结构(不能添加或删除元素)                         ,其元素是按需计算的        。

在哲学中          ,流被看作在时间中分布的一组值            ,而集合则是空间(计算机内存)中分布的一组值                        ,在一个时间点上全体存在                        。

只能遍历一次

和迭代器类似              ,流只能遍历一次                  。遍历完成之后        ,我们说这个流已经被消费掉了    。

例如下面的代码会抛出异常

Stream<Student> stream = students.stream(); stream.forEach(System.out::println); stream.forEach(System.out::println);

执行之后抛出如下异常:

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at com.example.demo.java8.stream.StreamTest.main(StreamTest.java:58)

所以要记得                        ,流只能消费一次                        。

外部迭代与内部迭代

我们使用iterator或者foreach遍历集合时的这种迭代方式被称为外部迭代                  ,而Streams库使用内部迭代    ,它帮你把迭代做了                        ,还把得到的流值存在了某个地方                      ,你只要给出一个函数说要干什么就可以了                      。

下面的代码说明了这种区别。

外部迭代 //使用增强for循环做外部迭代,底层还是迭代器 List<String> resultName = new ArrayList<>(); for (Student student : students) { resultName.add(student.getName()); } //使用迭代器做外部迭代 Iterator<Student> iterator = students.iterator(); while (iterator.hasNext()){ Student student = iterator.next(); resultName.add(student.getName()); } 内部迭代 List<String> resultName = students.stream() .map(Student::getName) .collect(Collectors.toList());

流操作

java.util.stream中的Stream接口定义了许多操作                    。可以分为两大类                          。先看一下下面这个例子:

List<String> resultName = students.stream() //从列表中获取流 .filter(student -> student.getAge() < 16) //中间操作 .map(Student::getName) //中间操作 .limit(2) //中间操作 .collect(Collectors.toList());//将Stream转为List

可以看到两类操作:

filter    、map和limit链接的一条流水线 collect触发流水线执行并关闭它

流水线中流的操作称为中间操作                    ,关闭流的操作称为终端操作 中间操作

诸如filter或sorted等中间操作会返回一个流                          ,这让很多操作链接起来形成一个复合的流水线(查询)     。重要的是     ,除非流水线上触发一个终端操作                ,否则中间操作不会执行任何处理——它们很

(延迟计算/惰性求值)                。

因为中间操作一般都可以合并起来                         ,在终端操作时一次性全部处理                         。

修改一下上面的代码          ,看一下发生了什么: List<String> resultName = students.stream() //从列表中获取流 .filter(student -> { System.out.println("filter:"+student.getName()); return student.getAge() < 16; }) //中间操作 .map(student -> { System.out.println("map:"+student.getName()); return student.getName(); }) //中间操作 .limit(3) //中间操作 .collect(Collectors.toList());//将Stream转为List

执行结果如下:

filter:张初一 map:张初一 filter:李初二 map:李初二 filter:孙初三 map:孙初三

可以发现            ,利用流的延迟性质实现了几个好的优化          。limit操作实现了只选择前3个                        ,filter和map操作是相互独立的操作              ,但他们合并到同一次遍历中            。

终端操作

终端操作会从流的流水线生成结果                        。其结果可以是任何不是流的值        ,例如List                        、Integer                        ,亦或是void等              。

流的使用

流的使用一般包括三件事:

一个数据源(如集合)来执行一个查询 一个中间操作链                  ,形成一条流水线 一个终端操作    ,执行流水线                        ,并生成结果

流的流水线背后的理念类似于构建器模式        。在构建器模式中有一个调用链来设置一套配置(对流来说这就是一个中间操作链)                      ,接着时调用build方法(对流来说就是终端操作)                        。

使用流

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"]              。

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

List<String> wordList = Arrays.asList("hello","word"); List<String[]> result = wordList.stream() .map(word -> word.split("")) .distinct() .collect(Collectors.toList());

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

List<String> wordList = Arrays.asList("hello","word"); List<Stream<String>> result = wordList.stream() .map(word -> word.split("")) .map(Arrays::stream) .distinct() .collect(Collectors.toList());

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

这时候flatMap就派上用场了:

List<String> wordList = Arrays.asList("hello","word"); List<String> result = wordList.stream() .map(w -> w.split("")) .flatMap(Arrays::stream) .distinct() .collect(Collectors.toList()); System.out.println(result);

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<T>对象     ,考虑流中没有任何元素                ,也没有初始值                         ,所以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 数值流例子

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

IntStream.rangeClosed(1, 100) .boxed() .flatMap(a -> IntStream.rangeClosed(a, 100) .mapToObj(b -> new double[]{a, b, Math.sqrt(a * a + b * b)}) .filter(ints -> ints[2] % 1 == 0)) .forEach(t -> System.out.println((int) t[0] + "--" + (int) t[1] + "--" + (int) t[2]));

构建流

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

由序列创建流 Stream.of

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

//创建一个字符串流 Stream<String> stringStream = Stream.of("A", "B", "C", "D", "E"); //创建一个空流 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<T>类型)     。这里是n -> n + 2                    ,返回前一个元素加2                。所以上面代码生成了一个正偶数流                         。如果不加limit限制                          ,则会永远计算下去          。

生成 Stream.generate

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

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

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
帝国cms到底好不好(帝国cms支持php吗)