前言

Java8中函数接口有很多,大概有几十个吧,具体究竟是多少我也数不清,所以一开始看的时候感觉一脸懵逼,不过其实根本没那么复杂,毕竟不应该也没必要把一个东西设计的很复杂。

几个单词

在学习了解之前,希望大家能记住几个单词,掌握这几个单词,什么3,40个官方的函数接口都是小问题了,不信的话接着往下看啦。ok,那这几个单词呢分别是supplier 提供者,consumer 消费者,function 函数,operation 运算符,binary 二元(就是数学里二元一次方程那个二元,代表2个的意思),双重的

四大基础函数接口

函数接口,你可以理解为对一段行为的抽象,简单点说可以在方法就是将一段行为作为参数进行传递,这个行为呢,可以是一段代码,也可以是一个方法,那你可以想象在java8之前要将一段方法作为参数传递只能通过匿名内部类来实现,而且代码很难看,也很长,函数接口就是对匿名内部类的优化。

  虽然类库中的基本函数接口特别多,但其实总体可以分成四类,就好像阿拉伯数字是无限多的,但总共就10个基本数字一样,理解了这4个,其他的就都明白了。

Functio<T,R>接口

function,顾名思义,函数的意思,这里的函数是指数学上的函数哦,你也可以说是严格函数语言中的函数,例如haskell里的,他接受一个参数,返回一个值,永远都是这样,是一个恒定的,状态不可改变的方法。其实想讲函数这个彻底将明白可以再开一篇博客了,所以这里不详细的说了。

   上面说到,函数接口是对行为的抽象,因此我方便大家理解,就用java中的方法作例子。

Fcuntion接口是对接受一个T类型参数,返回R类型的结果的方法的抽象,通过调用apply方法执行内容。

  1. public class Operation{
  2. /*
  3. 下面这个方法接受一个int类型参数a,返回a+1,符合我上面说的接受一个参数,返回一个值
  4. 所以呢这个方法就符合Function接口的定义,那要怎么用呢,继续看例子
  5. */
  6. public static final int addOne(int a){
  7. return a+1;
  8. }
  9. /*
  10. 该方法第二个参数接受一个function类型的行为,然后调用apply,对a执行这段行为
  11. */
  12. public static int oper(int a, Function<Integer,Integer> action){
  13. return action.apply(a);
  14. }
  15. /* 下面调用这个oper方法,将addOne方法作为参数传递 */
  16. pulic static void main(String[] args){
  17. int x = 1;
  18. int y = oper(x,x -> addOne(x));//这里可以换成方法引用的写法 int y = oper(x,Operation::addOne)
  19. System.out.printf("x= %d, y = %d", x, y); // 打印结果 x=1, y=2
  20. /* 当然你也可以使用lambda表达式来表示这段行为,只要保证一个参数,一个返回值就能匹配 */
  21. y = oper(x, x -> x + 3 ); // y = 4
  22. y = oper(x, x -> x * 3 ); // y = 3
  23. }
  24. }

这里的箭头指向的位置就是形参,可以看到第二个箭头的Lambda表达式指向了Funtion接口

Consumer 接口

Consumer 接口翻译过来就是消费者,顾名思义,该接口对应的方法类型为接收一个参数,没有返回值,可以通俗的理解成将这个参数'消费掉了',一般来说使用Consumer接口往往伴随着一些期望状态的改变或者事件的发生,例如最典型的forEach就是使用的Consumer接口,虽然没有任何的返回值,但是却向控制台输出了语句。

Consumer 使用accept对参数执行行为

  1. public static void main(String[] args) {
  2. Consumer<String> printString = s -> System.out.println(s);
  3. printString.accept("helloWorld!");
  4. //控制台输出 helloWorld!
  5. }

Supplier 接口

Supplier 接口翻译过来就是提供者,和上面的消费者相反,该接口对应的方法类型为不接受参数,但是提供一个返回值,通俗的理解为这种接口是无私的奉献者,不仅不要参数,还返回一个值,使用get()方法获得这个返回值

  1. Supplier<String> getInstance = () -> "HelloWorld!";
  2. System.out.println(getInstance.get());
  3. // 控偶值台输出 HelloWorld

Predicate 接口

predicate<T,Boolean> 谓语接口,顾名思义,中文中的‘是’与‘不是’是中文语法的谓语,同样的该接口对应的方法为接收一个参数,返回一个Boolean类型值,多用于判断与过滤,当然你可以把他理解成特殊的Funcation<T,R>,但是为了便于区分语义,还是单独的划了一个接口,使用test()方法执行这段行为

  1. public static void main(String[] args) {
  2. Predicate<Integer> predOdd = integer -> integer % 2 == 1;
  3. System.out.println(predOdd.test(5));
  4. //控制台输出 5
  5. }

其他的接口

介绍完正面这四种最基本的接口,剩余的接口就可以很容易的理解了,java8中定义了几十种的函数接口,但是剩下的接口都是上面这几种接口的变种,大多为限制参数类型,数量,下面举几个例子。

类型限制接口

  • 参数类型,例如IntPredicate,LongPredicate, DoublePredicate,这几个接口,都是在基于Predicate接口的,不同的就是他们的泛型类型分别变成了Integer,Long,Double,IntConsumer,LongConsumer, DoubleConsumer比如这几个,对应的就是Consumer<Integer>,Consumer<Long>,Consumer<Double>,其余的是一样的道理,就不再举例子了

  • 返回值类型,和上面类似,只是命名的规则上多了一个To,例如IntToDoubleFunction,IntToLongFunction, 很明显就是对应的Funtion<Integer,Double>Fcuntion<Integer,Long>,其余同理,另外需要注意的是,参数限制与返回值限制的命名唯一不同就是To,简单来说,前面不带To的都是参数类型限制,带To的是返回值类型限制,对于没有参数的函数接口,那显而易见只可能是对返回值作限制。例如LongFunction<R>就相当于Function<Long,R> 而多了一个To的ToLongFunction<T>就相当于Function<T,Long>,也就是对返回值类型作了限制。

数量限制接口

  • 有些接口需要接受两名参数,此类接口的所有名字前面都是附加上Bi,是Binary的缩写,开头也介绍过这个单词了,是二元的意思,例如BiPredicate,BiFcuntion等等,而由于java没有多返回值的设定,所以Bi指的都是参数为两个

Operator接口

  • 此类接口只有2个分别是UnaryOperator<T> 一元操作符接口,与BinaryOperator<T>二元操作符接口,这类接口属于Function接口的简写,他们只有一个泛型参数,意思是Funtion的参数与返回值类型相同,一般多用于操作计算,计算 a + b的BiFcuntion如果限制条件为Integer的话 往往要这么写BiFunction<Integer,Integer,Integer> 前2个泛型代表参数,最后一个代表返回值,看起来似乎是有点繁重了,这个时候就可以用BinaryOperator<Integer>来代替了。

下面是各种类型的接口的示意图,相信只要真正理解了,其实问题并不大

关于lambda的限制

Java8中的lambda表达式,并不是完全闭包,lambda表达式对值封闭,不对变量封闭。简单点来说就是局部变量在lambda表达式中如果要使用,必须是声明final类型或者是隐式的final例如

  1. int num = 123;
  2. Consumer<Integer> print = () -> System.out.println(num);

就是可以的,虽然num没有被声明为final,但从整体来看,他和final类型的变量的表现是一致的,可如果是这样的代码

  1. int num = 123;
  2. num ++;
  3. Consumer<Integer> print = () -> System.out.println(num);

则无法通过编译器,这就是对值封闭(也就是栈上的变量封闭)

如果上文中的num是实例变量或者是静态变量就没有这个限制。

看到这里,自然而然就会有疑问为什么会这样?或者说为什么要这么设计。理由有很多,例如函数的不变性,线程安全等等等,这里我给一个简单的说明

  • 为什么局部变量会有限制而静态变量和全局变量就没有限制,因为局部变量是保存在栈上的,而众所周知,栈上的变量都隐式的表现了它们仅限于它们所在的线程,而静态变量与实例变量是保存在静态区与堆中的,而这两块区域是线程共享的,所以访问并没有问题。

  • 现在我们假设如果lambda表达式可以局部变量的情况,实例变量存储在堆中,局部变量存储在栈上,而lambda表达式是在另外一个线程中使用的,那么在访问局部变量的时候,因为线程不共享,因此lambda可能会在分配该变量的线程将这个变量收回之后,去访问该变量。所以说,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了。

  • 严格保证这种限制会让你的代码变得无比安全,如果你学习或了解过一些经典的函数式语言的话,就会知道不变性的重要性,这也是为什么stream流可以十分方便的改成并行流的重要原因之一。

利用函数式接口优化代码

策略模式 lambda校验

  1. public interface ValidationStrategy {
  2. boolean execute(String s);
  3. }
  4. public class Validator {
  5. private final ValidationStrategy strategy;
  6. public Validator(ValidationStrategy strategy) {
  7. this.strategy = strategy;
  8. }
  9. /**
  10. * 给客户的接口
  11. */
  12. public boolean validate(String s) {
  13. return strategy.execute(s);
  14. }
  15. }
  16. Validator numbericValidator = new Validator((String s) -> s.matches("\\d+"));
  17. boolean res1 = numbericValidator.validate("1234");
  18. System.out.println(res1);
  19. Validator lowerCaseValidator = new Validator((String s) -> s.matches("[a-z]+"));
  20. boolean res2 = lowerCaseValidator.validate("abcde");
  21. System.out.println(res2);

责任链模式 lambda链式处理

  1. UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
  2. UnaryOperator<String> spellCheckProcessing = (String text) -> text.replace("labda", "lambda");
  3. Function<String, String> function = headerProcessing.andThen(spellCheckProcessing);
  4. String result = function.apply("Aren't labdas really sexy?!!");
  5. System.out.println(result);
  6. UnaryOperator<String> hhhhhProcessing = (String text) -> text.concat("hhhh");
  7. Function<String, String> function1 = function.andThen(hhhhhProcessing);
  8. String result1 = function1.apply("Aren't labdas really sexy?!!");
  9. System.out.println(result1);

对多个检验逻辑代码优化

在实际业务代码中,如果所需要的职责链判断条件很多,但每一个判断表达式传入相同参数,我们可以用BooleanSupplier或者BiPredicate接收lambda表达式来简化冗长的条件语句。

利用动态无参数Lambda表达式列表

  1. /** 获取审核结果方法 */
  2. private static AuditResult getAuditResult(AuditDataVO data) {
  3. List<BooleanSupplier> supplierList = new ArrayList<>();
  4. supplierList.add(() -> isPassed(data.getAuditItem1()));
  5. supplierList.add(() -> isPassed(data.getAuditItem2()));
  6. ...
  7. supplierList.add(() -> isPassed(data.getAuditItem11()));
  8. for (BooleanSupplier supplier : supplierList) {
  9. if (!supplier.getAsBoolean()) {
  10. return AuditResult.REJECTED;
  11. }
  12. }
  13. return AuditResult.PASSED;
  14. }

存在问题:

通过SonarLint插件扫描,没有提示任何问题。但是,每次都动态添加Lambda表达式,就会导致程序效率低下。那么,有没有把Lambda表达式静态化的方法呢?

利用静态有参数Lambda表达式列表

  1. /** 审核结果断言列表 */
  2. private static final List<Predicate<AuditDataVO>> AUDIT_RESULT_PREDICATE_LIST =
  3. Collections.unmodifiableList(Arrays.asList(
  4. data -> isPassed(data.getAuditItem1()),
  5. data -> isPassed(data.getAuditItem2()),
  6. ...
  7. data -> isPassed(data.getAuditItem11())));
  8. /** 获取审核结果方法 */
  9. private static AuditResult getAuditResult(AuditDataVO data) {
  10. for (Predicate<AuditDataVO> predicate : AUDIT_RESULT_PREDICATE_LIST) {
  11. if (!predicate.test(data)) {
  12. return AuditResult.REJECTED;
  13. }
  14. }
  15. return AuditResult.PASSED;
  16. }

适用条件:

  • 适合于&&(或||)连接大量条件表达式的情况;

  • 适合于每个条件表达式都需要传入相同参数data的情况,如果每个条件表达式传入参数不同,只能使用动态无参数Lambda表达式列表方法;

如果需要传入两个参数,可以使用BiPredicate类型来接收Lambda表达式;如果需要传入多个参数,则需要自定义方法接口。

总结

函数式接口(Functional Interface)是Java 8对一类特殊类型的接口的称呼。 这类接口只定义了唯一的抽象方法的接口,并且这类接口使用了@FunctionalInterface进行注解。在jdk8中,引入了一个新的包java.util.function, 可以使java 8 的函数式编程变得更加简便。这个package中的接口大致分为了以下四类:

  • Function: 接收参数,并返回结果,主要方法 R apply(T t)
  • Consumer: 接收参数,无返回结果, 主要方法为 void accept(T t)
  • Supplier: 不接收参数,但返回结构,主要方法为 T get()
  • Predicate: 接收参数,返回boolean值,主要方法为 boolean test(T t)

本篇介绍了四大函数接口和他们引申出的各类接口,终点是对不同种类行为的封装导致了设计出不同的函数接口,另外在使用函数接口或者lambda表达式的时候,要注意lambda对值封闭这个特性。

【Java 8】函数式接口(二)—— 四大函数接口介绍的更多相关文章

  1. [二] java8 函数式接口详解 函数接口详解 lambda表达式 匿名函数 方法引用使用含义 函数式接口实例 如何定义函数式接口

    函数式接口详细定义 package java.lang; import java.lang.annotation.*; /** * An informative annotation type use ...

  2. 开始Java8之旅(四) --四大函数接口

    前言   Java8中函数接口有很多,大概有几十个吧,具体究竟是多少我也数不清,所以一开始看的时候感觉一脸懵逼,不过其实根本没那么复杂,毕竟不应该也没必要把一个东西设计的很复杂. 几个单词   在学习 ...

  3. Java8函数之旅(四) --四大函数接口

    前言   Java8中函数接口有很多,大概有几十个吧,具体究竟是多少我也数不清,所以一开始看的时候感觉一脸懵逼,不过其实根本没那么复杂,毕竟不应该也没必要把一个东西设计的很复杂. 几个单词   在学习 ...

  4. “全栈2019”Java第六十二章:接口与常量详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  5. 一、HttpServletRequest接口 二、HttpServletReponse接口 三、POST和GET请求方式及其乱码处理 四、ServletContext对象和ServletConfig对象

    一.HttpServletRequest接口 内部封装了客户端请求的数据信息 接收客户端的请求参数.HTTP请求数据包中配置参数 ###<1>常用方法 getContextPath()重要 ...

  6. java线程池技术(二): 核心ThreadPoolExecutor介绍

    版权声明:本文出自汪磊的博客,转载请务必注明出处. Java线程池技术属于比较"古老"而又比较基础的技术了,本篇博客主要作用是个人技术梳理,没什么新玩意. 一.Java线程池技术的 ...

  7. Java之多线程方式二(实现Runnable接口)

    /** * 创建多线程的方式二:实现Runnable接口 * 1. 创建一个实现了Runnable接口的类 * 2. 实现类去实现Runnable中的抽象方法:run() * 3. 创建实现类的对象 ...

  8. java web学习总结(二十七) -------------------JSP标签介绍

    一.JSP标签介绍 JSP标签也称之为Jsp Action(JSP动作)元素,它用于在Jsp页面中提供业务逻辑功能,避免在JSP页面中直接编写java代码,造成jsp页面难以维护. 二.JSP常用标签 ...

  9. Java 8函数式接口functional interface的秘密

    Java 8函数式接口functional interface的秘密 2014年10月29日 17:52:55 西瓜可乐520 阅读数:3729   目录 [−] JDK 8之前已有的函数式接口 新定 ...

随机推荐

  1. 什么?还在用delete删除数据《死磕MySQL系列 九》

    系列文章 五.如何选择普通索引和唯一索引<死磕MySQL系列 五> 六.五分钟,让你明白MySQL是怎么选择索引<死磕MySQL系列 六> 七.字符串可以这样加索引,你知吗?& ...

  2. IDEA中三种注释方式的快捷键

    三种注释方式   行注释.块注释.方法或类说明注释. 一.快捷键:Ctrl + /   使用Ctrl+ /, 添加行注释,再次使用,去掉行注释 二.演示代码 if (hallSites != null ...

  3. silky微服务框架服务注册中心介绍

    目录 服务注册中心简介 服务元数据 主机名称(hostName) 服务列表(services) 终结点 时间戳 使用Zookeeper作为服务注册中心 使用Nacos作为服务注册中心 使用Consul ...

  4. Spark性能调优——9项基本原则

    原则一:避免创建重复的RDD 通常来说,我们在开发一个Spark作业时,首先是基于某个数据源(比如Hive表或HDFS文件)创建一个初始的RDD:接着对这个RDD执行某个算子操作,然后得到下一个RDD ...

  5. JDBC操作多张表一

    一.操作一对多情况开发步骤1创建对象 //代码部门的对象public class Department { private String id; private String name; privat ...

  6. 【华为昇腾】 序言:从昇腾AI软硬件平台聊起

    2021年是很值得纪念的一年,从上半年开始跟随导师编写有关华为昇腾软件栈CANN的教材,一年的时间反复迭代 终于快要出版了. 这一系列博客可以视作我从编者的角度,重新梳理的全书思路.明年入职商汤之后要 ...

  7. [atARC080F]Prime Flip

    构造一个数组$b_{i}$(初始为0),对于操作$[l_{i},r_{i}]$,令$b_{l_{i}}$和$b_{r_{i}+1}$值异或1,表示$i$和$i-1$的差值发生改变,最终即要求若干个$b ...

  8. 如何在C#中使用Google.Protobuf工具

    protobuf是一个语言无关.平台无关的序列化协议,由谷歌开源提供.再加上其高性能.存储占用更小等特点,在云原生的应用中越来越广泛. 在C#中主要有两种方法来使用protobuf协议,nuget包分 ...

  9. 多声部处理细节之crossstaff音符处理

    下面给出这几个案例的代码 ,可直接粘贴编译. \version "2.20.0" \language "english" \paper { #(set-pape ...

  10. watch异步操作

    异步操作: 1.ajax, 2.定时器 3.点击事件 4.数据库操作 特点:代码不等待,后续代码会继续执行. watch:{ //watch作用监测已经存在的数据 newVal 新值,oldVal 旧 ...