目录
(5)anyMatch()、allMatch()、noneMatch()
之前系统学过的一个东西了,但只学而不总结,总觉得少点什么。
函数式编程思想
函数式编程思想中的函数如同数学中的函数,主要关注对数据进行了什么操作。
代码简洁、开发迅速,接近自然语言易于理解,易于并发编程。
java Stream是函数式编程思想的一种实现。
Lambda表达式,是函数式编程的基石(所以这里先介绍Lambda表达式的写法),是推动java8发布的最重要特性。
Lambda表达式,是函数式编程的一个重要体现,不用关注对象,而是关注对数据进行了什么操作。
Lambda运行把函数作为一个方法的参数,将函数作为参数传递进方法中。
Lambda表达式,可以对某些匿名内部类的写法就进行简化,能使代码变的更加简洁紧凑。
Lambda 表达式,免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
(参数列表)-> {代码}
示例1:
创建线程并启动时的匿名内部类的写法:
匿名内部类的执行过程:
- 程序执行到第7行,new一个Thread对象,开始执行Thread类的构造方法;
- Thread类的构造方法中有一行执行了target.run()方法,实际是执行了Runnable的run()方法;
- 而这个target是Runnable对象,是new Thread(new Runnable() {中传入的由new Runable()创建的Runable对象;
- 且,这个传入的Runable对象重写了run()方法。
- 于是,调用了第9行的public void run()方法,就执行了第10行代码
System.out.println("线程执行!");总之,执行外层方法时,外层方法内部调用了传入的且重写了方法的匿名内部类的方法,使重写的匿名内部类的方法中的代码被执行了。
Lambda表达式的写法:
示例2:
匿名内部类的写法:
Lambda表达式写法:
从上面两种写法可以看出,二者的效果是等效的,但Lambda的写法的代码更加简洁、紧凑。
Lambad语法格式:(参数列表)-> {代码}
对于idea,写完匿名内部类——>光标在匿名内部类名——>Alt+Enter——>选中“Replace with lambda”——>idea就自动把匿名内部类的写法转换成Lambda表达式的写法!
写成Lambda表达式的条件:
Lambda表达式只支持函数式接口(也就是只有一个抽象方法的接口),所以只有匿名内部类是只有一个抽象方法的接口才能换成Lambda表达式的写法。
这个通过idea的Alt+Enter转化的方法是在对Lambda表达式的写法不熟悉的时候的一个上上策,当熟悉之后便可以一气呵成!
Stream,流,Java8的新特性,在java8中添加的一个新的接口。Stream的诞生,极大提高了java的生产力,让开发人员写出高效率、干净、简洁的代码。
Stream将要处理的元素看作流,在管道中传输,并在管道的节点上由中间操作进行处理,比如排序、筛选、聚合等,最后经过最终操作得到前面处理的结果。
Stream中的元素是特定类型的对象,一个一个通过各种操作(中间操作和最终操作)进行计算,它并不会存储元素。
Stream的机制使程序具有以下的特点:
Stream流,支持顺序和并行聚合操作一系列元素
Stream流,使用的是函数式编程的模式;
Stream流,可以被用来对集合或数组进行链状流式的操作;
Stream流的操作分为中间操纵和终端操作,组合一起以形成流管道。
流管道由数据源、零个或多个中间操作和一个终端操作组成。
数据源是流的数据的来源,可以是集合、数组等,结合生成流的操作将数据输入流管道。
中间操作会返回一个新的流,多个中间操作串联成一个管道,在管道的终端操作被执行之前,中间操作不会开始。
在执行终端操作之后,流管道被消耗,不能在被使用;如果需要再次使用,则必须返回到数据源获取新的流。
流通过将计算重构为聚合操作的管道来实现并行执行;所有流操作可以串行或并行执行。
流上的操作产生结果,但不会修改其来源。
流不是存储元素的数据结构; 相反,它通过计算操作的流水线传送诸如数据结构,数组,生成器功能或I / O通道的源的元件。
Stream流的特点:
- 惰性求值,如果没有终端操作,中间操作不会执行;
- 流是一次性的,一个流对象经过一个总结操作后,该流就不能再用了;
- 不影响源数据,正常情况下,在流中的操作不影响原来集合中的元素。
执行过程:
通过List的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
通过Arrays的静态方法Arrays.stream()方法将数组的元素作为数据源输入流管道,返回Stream对象;
通过Stream的静态方法Stream.of()方法将数组的元素作为数据源输入六管道,返回Stream对象。
获取流的几种方式:
- Collection:通过Collection.stream()和Collection.parallelStream()方法
- Arrays:通过Arrays.stream(Object[])
- 流类静态工厂的方法
- Stream.of(Object[])
- IntStream.range(int, int)
- Stream.iterate(Object, UnaryOperator)
- 文件的行可以从BufferedReader.lines()获取
- 文件路径的流:可以从Files中的方法获得
- 随机数流:可以从Random.ints()获得
- 许多其它的数据流的方法
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
串行和并行版本之间的唯一区别是创建初始流时,使用“ parallelStream()
”而不是“ stream()
”。
除了
Stream
,对象引用的流,还有支持特定类型的元素的流IntStream、DoubleStream、LongStream。
终端操作根据操作的不同含义返回不同类型的值或void。
遍历流中的元素,为该流的每个元素执行一次传入的操作。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用forEach()方法,将流管道的元素经过forEach()并执行传入的System.out.println操作。
运行结果:
获取当前流中元素的个数。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用count()方法,将流管道的元素经过count()统计该流中元素的个数。
运行结果:
当然,当只需要获取集合中的元素个数时,没必要通过转换成流在调用count()方法,一般不单独使用而是结合中间操作使用。
根据提供的Comparator返回该流的最大元素。
根据提供的Comparator返回该流的最小元素。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用max()方法,将流管道的元素经过max()并根据传入的Comparator.comparingInt()比较器找到流中元素的最大值并返回。
执行结果:
把当前流中的元素收集起来转换成一个集合返回。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用collect()方法,将流管道的元素经过collect()并根据传入的Collectors收集器将流元素转换成指定类型集合并返回。
执行结果:
判断该流中是否有元素符合传入的条件,如果有返回true。
判断该流中的所有元素是否符合传入的条件,如果全都符合返回true。
判断该流中的所有元素是否都不符合传入的条件,如果全都不符合返回true。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用anyMatch()方法,将流管道的元素经过anyMatch()并根据传入的判断条件,判断流中是否有元素大于12,因为没有流中所有元素都不大于12,所以返回false。
执行结果:
返回流中任意一个元素,如果流为空就返回 一个空的Optional对象。
返回流中第一个元素,如果流为空就返回 一个空的Optional对象。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用findAny()方法,返回流管道的任意一个元素。
执行结果:
将流中的元素按照传入的计算方式计算出一个结果。
其中,identity是传入的初始值;实现accumulator的apply()方法,指定具体进行的计算。
如果传入一个这个identity初始值,会按照实现的计算方式将流中的元素在初始值的基础上进行计算;
如果不传入这个identity初始值,会将流中的第一个元素作为初始值,在此基础上进行计算。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用reduce()方法,根据传入的Integer::sum操作,将流管道中的元素相加,并将结果返回。
执行结果:
具体细节参见手册。
中间操作返回一个新的流。可链式调用下一个中间操作或终端操作。
对流中的元素进行条件过滤,匹配过滤条件的元素被本方法返回,继续留在流中。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用fliter()方法,根据传入的判断操作,过滤流管道中的元素,将符合条件的保留,在流管道中,该中间操作fliter将符合条件的元素返回Stream对象;
该Stream对象调用终端操作forEach(),将流管道的元素经过forEach()并执行传入的System.out.println操作。
执行结果:
根据传入的操作Function,将流中的元素计算或转换成对应的结果。
对于Double、Int、Long类型的元素,提供了特定的map()方法。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用map()方法,根据传入的计算操作,将流中的元素一一计算其对应的平方数,该中间操作map()将计算的结果返回Stream对象;
该Stream对象调用终端操作forEach(),将流管道的元素经过forEach()并执行传入的System.out.println操作。
执行结果:
去除流中重复的元素,返回去重后的元素的流对象。
内部根据Object.Equals(Object)方法比较元素是否相同。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用distinct()方法,除去流中的重复元素,该中间操作distinct()将去重后的结果返回Stream对象;
该Stream对象调用终端操作forEach(),将流管道的元素经过forEach()并执行传入的System.out.println操作。
执行结果:
将流中的元素进行排序,返回排序后的流对象。
如果不传参数,就按照自然顺序排序,但流中的元素要已经实现了Comparable接口;如果传入Comparator比较器对象,就根据传入的操作排序。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用sort()方法,根据传入的比较条件(如果不传默认以自然顺序)对流中的元素进行排序,该中间操作sort()将排序后的结果返回Stream对象;
该Stream对象调用终端操作forEach(),将流管道的元素经过forEach()并执行传入的System.out.println操作。
执行结果:
根据输入的长度,截取不超过maxSize长度个元素,返回截取后的流对象。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用limit()方法,截取传入数字个元素,该中间操作sort()将截取元素后的结果返回Stream对象;
该Stream对象调用终端操作forEach(),将流管道的元素经过forEach()并执行传入的System.out.println操作。
执行结果:
丢弃流的前n个元素后,返回一个包含该流剩余元素的流。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用skip()方法,丢弃传入数字个元素,该中间操作skip()将丢弃元素后的结果返回Stream对象;
该Stream对象调用终端操作forEach(),将流管道的元素经过forEach()并执行传入的System.out.println操作。
返回一个流,其中包含将该流的每个元素替换为 将提供的映射函数应用到每个元素所产生的映射流的内容的结果。每个被映射的流在其内容被放置到这个要返回的流之后被关闭。
——流中的每个元素经过传入的操作处理后产生一个流,每个元素经过传入的操作处理产生的流中的元素合并到返回的流中。
——flatMap()操作的效果是对流的元素应用一对多转换,然后将产生的元素扁平化到新的流中。
根据传入的操作Function,将流中的元素计算或转换成对应的结果。
对于Double、Int、Long类型的元素,提供了特定的map()方法。
map()只能把一个对象转换成另一个对象来作为流中的元素;而flatMap()可以把一个对象转换成多个对象作为流中的元素,从flatMap()的参数可以看出抽象方法的返回值是Stream,而map是一个泛型。
泛型R是返回的新流中的元素类型。
示例一:
如果订单是一个采购订单流,并且每个采购订单都包含一行项目的集合,那么下面将生成一个包含所有订单中的所有行项目的流:
orders.flatMap(order -> order.getLineItems().stream())...
示例二:
如果path是一个文件的路径,那么下面的代码将生成该文件中包含的单词流:
Streamlines = Files.lines(path, StandardCharsets.UTF_8); Streamwords = lines.flatMap(line -> Stream.of(line.split(" +"))); 传递给flatMap的mapper函数使用一个简单的正则表达式将一行分割成一个单词数组,然后从该数组创建一个单词流。
执行过程:
通过List对象的stream()方法将List集合中的数据作为数据源输入流管道,返回Stream对象;
该Stream对象调用flatMap()方法,相当于把lists中的三个元素numbers、numbers2、numbers3分别传入到apply方法,在apply()方法中直接返回numbers、numbers2、numbers3这三个List的Stream对象,flatMap将这三个Stream对象中的元素(即是numbers、numbers2、numbers3这三个List的中的元素)分别合并到flatMap()方法要返回的Stream对象中;该中间操作flatMap()返回这个Stream对象。
该Stream对象调用collect()方法,将流管道的元素经过collect()并根据传入的Collectors收集器将流元素转换成指定类型集合并返回。
执行结果:
具体细节参见手册。
以上的终端操作和中间操作都是以Stream串行流演示的,对于并行流ParallelStream来说,终端操作和中间操作的方法的使用基本与Stream串行流一致;
但有一部分方法因为流被分成多个流并行执行会出现不同的结果;比如limit(),就不会在像串行流一样按顺序返回流中的前几个元素了;
但因为并行执行了,效率往往会更快。
虽然Stram流执行过程都是方法的调用,我们又不太好进入方法内部查看执行过程,而且流代码的书写格式都是级联调用的,所以平常所用的单步执行不好去调试流,但,idea对Stream流的调试有非常好的支持!
在流的代码上打上断点——>点击Debug——>在点击这个位置:
弹出一个教Stream Trace的弹框:
在这里,我们可以方便的查看数据源、中间操作、终端操作的每个过程元素的形式,每个方法执行完元素的形式。即可以查看元素在整个管道的经历的过程。
可以切换显示模式,以不同的形式显示元素在管道的经历。
我们在编写代码的时候经常需要进行很多非空指针的判断,太多的判断语句会让代码显得复杂、不直观,并且要人为保证所有的情况都进行了判断;
JDK8中就引入了Optional类,通过使用该类可以更方便的避免空指针异常的情况;
而很多函数式编程的相关API(比如前文中的findAny()、findFirst()、max()、min()等)都使用了Optional,所以学习该类有助于我们对函数式编程的理解和使用。
Optional 类是一个可以为null的容器对象。
Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Optional 类的引入很好的解决空指针异常。
在返回数组的方法中,将方法的返回值用Optional封装,在接收方法的返回值是Optional对象,再去用该对象的判空方法判断,以实现优雅的避免空指针的方式。
在实际开发中,数据往往是从数据库中获取,从Mybatis3.5以上就支持了Optional类,可以直接将Dao层方法的返回值类型定义成Optional类型,Mybatis会自动将数据封装成Optional对象返回,我们在接收到该对象之后采用该对象的判空方法。
①ofNullable()
如果传入的value为非空,返回封装了value值的Optional对象,否则返回空的 Optional对象。
无论传入的参数是否为null都没有问题。
推荐使用!因为使用前不用关注传入的值是否为空。
②of()
如果传入的value为非空,返回封装了value值的Optional对象,否则在使用这个返回的Optional对象时会报空指针异常的问题。
③empty()
返回空的 Optional对象。
三个方法都是Optional类的静态方法。
①get()
如果该Optional对象中有值,则返回该值,否则抛出NoSuchElementException异常。
当Optional对象内部的数据为扣扣那个时出抛出异常。
不推荐使用。
运行结果:
②orElseGet()
如果该Optional对象中有值,则返回该值, 否则触发传入的other,并返回 other 调用的结果。
Optional对象不为空时返回数据,如果为空时根据传入的参数设置并返回默认值。
运行结果:
③orElseThrow
如果该Optional对象中有值,则返回该值, 否则根据传入的参数创建异常抛出。
运行结果:
①ifPresent()——重要
这个方法判断Optional对象中封装的数据是否为空。
如果值存在,则使用该值调用 consumer,执行相关代码 , 否则不做任何事情。
运行结果:
②isPresent()
对Optional对象是否存在数据进行判断,如果该Optional对象中有值,则返回true, 否则返回false。
这种方式下的代码又退化成了if判断,不能体现Optional的精髓。
运行结果:
过滤Optional对象中的数据,如果该数据匹配传入的条件,则返回该数据的Optional对象,否则返回 一个空的Optional对象。
运行结果:
如果有值,则对其执行调用传入的映射函数得到返回值。如果返回值不为 null,则创建包含映射返回值的Optional作为map方法返回值,否则返回空Optional。
map()和flatMap()方法与Stream中的方法异曲同工。
运行结果:
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
JDK的函数式接口上通过@FunctionalInterface注解进行了标识。
无论是否加上该注解,只要只有一个抽象方法的接口,都是函数式接口。
具体使用时,我们很容易可以通过idea进入接口文件,通过阅读其注释可以很清楚的知道抽象方法的功能、参数、返回值等信息。
而一般情况下,我们在重写方法时看到idea自动生成的代码基本就知道了该接口的使用方法。
当然,函数式接口一个抽象方法外,还有一些默认的方法and()、andThen()、or()、negate()等,通常有default关键字,一般不常用,如果需要的话可以通过idea的.(点)提示出来,同样可以点进方法文件查看一下方法的具体信息。
在使用Lambda时,如果实现的抽象方法的方法提中只有一个方法的调用的话,可以用方法引用的方式调用这个方法。
方法引用通过方法的名字来指向一个方法。
方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
方法引用使用一对冒号 :: 。
类名或者对象名::方法名
引用类的构造方法。
如果在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的构造方法,并且我们要把重写的抽象方法中所有的参数都按照顺序传入这个构造方法中,此时可以使用方法引用。
Class名:new
引用类的静态方法。
如果在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的静态方法,并且我们要把重写的抽象方法中所有的参数都按照顺序传入这个静态方法中,此时可以使用方法引用。
Class名:静态方法名
如果在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了第一个参数的成员方法,并且我们要把重写的抽象方法中剩余的所有参数都按照顺序传入这个成员方法中,此时可以使用方法引用。
Class名:方法名
如果在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个对象的成员方法,并且我们要把重写的抽象方法中剩余的所有参数都按照顺序传入这个成员方法中,此时可以使用方法引用。
对象名:方法名
就像我们前期写Lambda表达式一样,没有必要完全背下写法之后开始写,同样,前期可以通过idea的Alt+Enter快捷键提示,如果有”Replace with method reference"一项, 选择这一项自动转换即可。
当然,知道基本规则之后,用多了,就可以直接写出方法引用啦。
温故而知新,系统总结完之后,感觉内容过更加清晰了,心是莲花开!