月朦胧

V1

2022/11/29阅读:21主题:重影

关于java8函数式接口的思考之异步与委托

关于java8函数式接口的思考之异步委托

什么是函数式接口

仅有一个抽象方法,可以拥有多个非抽象方法的的接口。

为什么java8有函数式接口

首先,我们需要明白函数是什么,在数学中函数通俗的意思就是由自变量和因变量所确定的一种关系。而在计算机中,函数则是是一个固定的一个程序段,它在可以实现固定运算功能,并且提供入参和结果出参(入参和出参非必须),可以通俗的理解为Java中的方法。

public int sum(int a, int b){
    return a + b;
}

在java这个面向对象的语言里,什么都可以被当作一个对象来描述,甚至是一个简单的用户名username,在需要的适合也可以封装成一个UserName对象,拥有自己的构造方法和逻辑方法,甚至可以在构造方法中完成构造我(UserName)需要的参数以及校验(例如"我"不能包含敏感字符)。

那么对于一个函数(方法)也不例外,它也可以被描述成一个对象

public class Sum{
  
    public int sum(int a, int b){
        return a + b;
    }
}

在函数的基础上,为了多态扩展,函数会当作函数式接口来做不同实现

public interface Sum{
    
    int sum(int a, int b);
}

一般情况下,接口实现都是通过创建一个类,implements接口的方式来实现的,而这种方式在函数(逻辑片段)过多的情况下,如果都去实现一遍,往往会造成存在大量的类,影响管理上的混乱和使用上的不便。

于是在java8之前,java的做法是接口的匿名实现,可以在代码中快速实现一个接口的匿名(临时的实现变量,没用具体继承自接口的类)实现,例如我们在创建线程Thread时常用的参数 Runnable接口,在Thread构造方法中,我们传递的runnable实现变成了Thread的一个成员变量,最终在调用Thread的run方法时(也可以通过start()方法,但是这个方法是调用了native方法去启动的)方法时,run()里调用了runnable的run方法。

源码示例,具体源码可以自行阅读Thread源码:

使用方法:

    public void testTheadRunnable() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名实现了一个runnable接口");
            }
        }).start();
        //等同于
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名实现了一个runnable接口");
            }
        };
        new Thread(runnable).start();
    }

但是!! 对于这样的实现方式,虽然说没有问题,但是代码冗长,写法难看(很容易被其它语言开发者鄙视),于是乎,java8对这种场景做了语法糖优化,这种语法糖优化在java8中被称为lambda (此处简单介绍,向后阅读详细介绍)。

lambda语法其实就是匿名实现了一个函数式接口:


    public void testTheadRunnable1() {
        new Thread(() -> System.out.println("Lambda 实现了一个Runnable函数式接口")).start();
        //等同于
        Runnable runnable = () -> System.out.println("Lambda 实现了一个Runnable函数式接口");
        new Thread(runnable).start();
    }

重点来了!!

一个接口可以拥有多个抽象方法,如果拥有了多个抽象方法,那么它就不满足于计算机中函数"一个固定的一个程序段"的定义。

基于编译语言和复杂度的问题上,为了满足语法糖的推导,于是乎java8中引入了

@FunctionalInterface 注解标识一个函数式接口,该注解会在编译期对接口是否满足函数式接口进行检查:

所以,函数式接口的概念也并非从java8开始的,只不过为了方便lambda语法糖的推导,由是更加强调这个概念,并且给予规范。

函数式接口和lambda的使用

Lambda 表达式可以通过 匿名实现方法引用 的语法糖进行匿名传递。

匿名实现

匿名实现,可以分为单行实现和多行实现。

//单行实现
new Thread(() -> System.out.println("Lambda 实现了一个Runnable函数式接口")).start();
//多行实现
new Thread(() -> {
  System.out.println("Lambda 实现了一个Runnable函数式接口");
  System.out.println("Lambda 实现了一个Runnable函数式接口");
  System.out.println("Lambda 实现了一个Runnable函数式接口");
}).start();

在单行实现下,不需要方法体大括号,也不需要语句结束的";"号结尾。

且如果匿名实现的是一个有返回值的函数式接口,会隐式的添加上 return语句。

@FunctionalInterface
public interface Supplier<T{

    /**
     * Gets a result.
     *
     * @return a result
     */

    get();
}
    public void testReturn() {
        Supplier<Integer> supplier1 = () -> new Integer(666);
        Integer result1 = supplier1.get();
        //等同于
        Supplier<Integer> supplier2 = () -> {return new Integer(999);};
        Integer result2 = supplier2.get();
    }

无论是单行匿名实现或者是多行匿名实现,lambda表达式前面的() 括号则代表你要匿名实现的函数式接口的抽象方法的入参,如果是无参数抽象方法则是"()->",有参数则是"(参数)->",单参数可被简化为 var -> 的方式。 示例:

无参数示例

对应

有参数示例

@FunctionalInterface
public interface Sum {
    int sum(int a, int b);
}

单参数

对应


方法引用

可以通过方法引用来匿名实现一个函数式接口,语法

对象::方法  或  静态类::静态方法
    @FunctionalInterface
    public interface Requester {
        void request();
    }
    
    public void testRefVar() {
        //引用runMethod方法,匿名实现了executeMethod 形参的Requester函数式接口
        this.executeMethod(this::runMethod);
    }

    public void executeMethod(Requester requester) {
        requester.request();
    }

    public void runMethod() {
        System.out.println("请求中...");
        System.out.println("请求完成!");
    }

被引用的方法需要和匿名实现的函数式接口的抽象方法具有相同的入参出参。

    @FunctionalInterface
    public interface Sum {
        int sum(int a, int b);
    }

    public void testRefVar() {
        //引用runMethod方法,匿名实现了executeMethod 形参的Sum函数式接口
        this.executeMethod(this::runMethod);
    }

    public void executeMethod(Sum sum) {
        int x = 6;
        int y = 9;
        sum.sum(x, y);
    }

    public int runMethod(int a, int b) {
        return a + b;
    }

函数式编程的思维提升 委托与异步编程

到此为止,我们需要思考一个问题,函数式接口能为我们带来哪些编程思维上提升呢?从我个人的理解上,可以总结为两点

  1. 委托
  2. 异步编程

委托:如其名意,在java中可以理解为,某个对象A将某个逻辑委托给调用其方法的B对象来实现。

异步编程:这里的异步并非开启一个编程异步执行的意思,而是指你在现实编码方式的异步,指的是不必按照一连串代码的串行编码。在写某段逻辑触发的代码时,这段代码片段已经提前被写好了,还没有被执行,只是在需要的时候执行调用。

举个例子,类似前端编程语言中常见的回调方法,B对象调用A对象的A1方法,并且在方法参数中传递了一个回调方法,A对象A1方法中在某种场景下执行了B对象传递的回调方法,这个场景在前端语言中非常常见。而在Java中语言编程过程中,其实也有常见的写法,类似设计模式中的观察者模式等。

然而一个逻辑就要通过设计模式的方式来做,无疑有点大炮打小鸟的意味。而利用函数式接口,则可以快速清晰的实现,举例上述场景:

    public static class A {
        public void a1(Runnable call) {
            //do something
            System.out.println("只因泥太煤");
            if (Boolean.TRUE) {
                call.run();
            }
        }
    }

    public static class B {
        public void b1() {
            A a = new A();
            a.a1(() -> System.out.println("小黑子真虾头"));
        }
    }

在这个简单的例子当中,委托和异步编程体现在哪里呢?

委托:A对象的a1方法将一个回调方法的实现委托给调用其的对象自己实现,A对象的a1方法中,仅有自己的逻辑,不包含调用其方法的其他方法的业务逻辑,职责分明。

异步编程:回调方法由B对象已经提前写好了,在写这段代码时,这个回调方法并未被执行,而是A对象的a1调用时,才真正被执行。对于A对象和B对象来说,在编码上这无疑都是异步的。

在Java8提供的一些开发工具类中,我们能很频繁的见到这种使用方式,相信大家在日常开发过程中肯定也是高频使用,举例我们在java8常用的Optional

Optional

ifPresent方法,Optional只完成存在则执行的职责,具体的执行逻辑通过Consumer函数式接口参数委托给调用者自己实现。

orElseGet方法,Optional只完成"获取和不存在则获取"的职责,将不存在的获取方式通过Supplier函数式接口参数委托给调用者自己实现。

orElseThrow方法,Optional只完成"获取和不存在则抛出异常"的职责,将不存在需要抛出的异常通过Supplier函数式接口参数委托给调用者自己来生成。

使用示例:

    public void testOptional(Object obj) {
        Optional<Object> objOptional = Optional.ofNullable(obj);
        //如果存在执行
        objOptional.ifPresent(o -> System.out.println(o.toString()));
        //如果不存在调用supplier获取
        Object o = objOptional.orElseGet(Object::new);
        //如果不存在,调用supplier获取异常抛出
        Object o1 = objOptional.orElseThrow(() -> new RuntimeException("空空如也"));
    }

代码简洁干净,言简意骇,举个"反例":

    public void testOptional1(Object obj) {
        //如果存在执行
        if (obj != null) {
            System.out.println("666");
        }
        //如果不存在获取
        Object object = null;
        if (obj != null) {
            object = obj;
        } else {
            object = new Object();
        }
        //如果不存在,则抛出异常抛出
        Object object1 = null;
        if (obj != null) {
            object1 = obj;
        } else {
            throw new RuntimeException("空空如也");
        }
    }

甚至,你还可以利用这些玩一些"花活",我们经常在代码的碰到过同类方法调用时,如果被调用的方法是利用spring的aop来做代理实现的情况时,例如@Transactional 或 @Cacheable,往往会因为同类调用无法代理导致失效(虽然这种情况往往是因为自己层级职责划分错误导致),一旦遭遇这种问题,大部分人的做法是,将方法拆到另一个类中或者手动从ioc容器中获取或者注入bean来完成代理。而在这里,我们其实通过函数式接口委托的方式,来做一个统一的代理类。

示例:

@Component
public class TransactionalManager {
    @Transactional
    public void runTransaction(Runnable runnable){
        runnable.run();
    }
}

改为增加一个TransactionalManager

@Component
public class Test {
    @Autowired
    private TransactionalManager transactionalManager;
    
    public void a(){
        //代理失败,因为spring是通过aop代理实现的。
        this.b();
        //事务代理成功
        transactionalManager.runTransaction(this::b);
    }

    @Transactional
    public void b(){
        System.out.println("事务");
    }
}

通过实现一个事务的Manager类,职责就是帮助完成事务代理,这样b方法就可以不用拆到别的类当中去了。

题外话,如果跳出Java这个编程文化圈来看,如果是写过前端JavaScript的小伙伴, 其实对这种函数编程其实是如同吃饭喝水一样习惯了,因为前端js为了用户体验,渲染速度等,一定是经常使用异步的,而异步回调上经常也是通过传递函数实现的,写过js的同学一定使用过非常好用的东西:Promise, Promise可以很好的解决异步回调的问题,例如:

 var promise = new Promise(function(resolve, reject){
        //异步
        setTimeout(function(){
            resolve('异步完成,执行回调');
        }, 2000);
    });
promise.then(res=> console.log(res))

//输出'异步完成,执行回调'

你不理解JavaScript其实也没关系,也不Promise工作原理,但是你从共通代码其实不难看出,其就是基于函数委托给Promise执行回调来工作的,不能的语言,其实思想上很多都是相通的。

分类:

后端

标签:

Java

作者介绍

月朦胧
V1