关于Java和Kotlin的函数式编程

409 阅读12分钟

0.前言

函数式编程:

  1. 是一种区别与面向对象的开发方式
    1. 面向对象针对于一个业务实体 或 一组业务实体 进行抽象封装。业务实体的数据变为类属性,业务实体的行为变为类方法。在面向对象中函数(方法)并不能独立存在,必须依附于某个类才能被调用。面向对象倾向于组织系统结构
    2. 函数式编程是对计算的抽象,倾向于分隔问题,根据实际情况把代码划分为一个个函数。不需要属性,不需要new对象,不需要繁琐的语法,包含几行或十几行的代码块才是关键。
      1. 偏工具类的例子:字符串分隔的算法。
      2. 偏业务的例子:根据用户状态展示不同的图标
  2. 语言层面
    1. 函数也是一种类型,可以引用,当作参数传递,可以把一块代码赋值给变量
    2. 函数可以脱离类直接定义,使用,函数与类是平等关系
  3. 函数式编程和面向对象编程之间绝对不冲突。 你比我好,我比你强,一定要分个高下。这种想法是绝对错误的,它们是互补的。
    1. 构建一套企业办公系统,雇员,组长,经理,总监,老板。每个角色有不用的权限,不用的行为。很明显这活就得用面向对象来干。
    2. 如果系统中需要一个简单的发邮件功能。当然是直接调用一个函数更方便了。
  4. 函数式以好面向对象也罢,关键是把握它们的思想。 用java就一定写的是面向对象代码么?用js就没办法合理封装抽象业务实体么?难说呀 🐶

1.函数式编程与Java

Lambda 表达式有何用处?如何使用? - Mingqi的回答 - 知乎 www.zhihu.com/question/20…

☕️【Java技术之旅】带你看透Lambda表达式的本质_Java_浩宇天尚_InfoQ写作平台

Java8前

在Java8之前是不支持函数式编程的。从语法上说不存在函数类型,函数只能通过对象调用,没办法直接引一个函数,赋值给某个变量。

模拟一个简单需求,定义加法函数,传入参数a,b返回两数相加的和。Java8之前开发人员通过接口达到类似的效果。

代码如下:

  1. 定义接口IAddFunction
  2. 定义方法:private void test(IAddFunction addFunction) 参数是接口 IAddFunction
  3. test方法内部,模拟逻辑,调用IAddFunction 接口实现两数相加。
  4. onCreate方法中,通过创建匿名内部类 和 接口实现 的方式 向test方法传递引用。
  5. 如下实现虽然达到了 test方法中 调用 add方法 的效果。但是add()仍然是一个类方法。使用时必须通过对象调用方法,而不是直接调用方法
  6. java8之前的实现非常繁琐,明明只需要add()代码块中简单的a+b 。却需要定义接口,实现类

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //1.匿名内部类
    test(new IAddFunction() {
        @Override
        public int add(int a, int b) {
            return a + b;
        }
    });
    //2.接口实现类
    IAddFunction addFunction = new IAddFunctionImpl();
    test(addFunction);
}

/**
 *测试方法
*/
private void test(IAddFunction addFunction) {
    //假装是一个网络请求
    //假装获取到了两个用户信息
    //计算两个用户的钱包余额总数
    int sum = addFunction.add(18, 20);
    Log.d(TAG, "余额总数:" + sum);
}

/**
 *接口实现类
*/
public static class IAddFunctionImpl implements IAddFunction {

    @Override
    public int add(int a, int b) {
        return a + b;
    }
}

/**
 *加法函数接口
*/
interface IAddFunction {
    int add(int a, int b);
}

除接口实现之外,还有另一种简单且广为流程的方式,存在于各种工具类中的静态方法。

虽然依旧无法引用方法,但是可通过类名直接调用方法,使用简单。

public void test2(){
    //假装是一个网络请求
    //假装获取到了两个用户信息
    //计算两个用户的钱包余额总数
    int sum = Utils.add(18, 20);
    Log.d(TAG, "余额总数:" + sum);
}

public class Utils {
    public static int add(int a, int b) {
        return a + b;
    }
}

以上两种方式:接口实现,静态方法,是Java8之前函数式编程的一种体现,是开发人员为了满足直接调用一段代码块(函数)需求的便宜实现。

归根究底原因在于Java8之前,java是一门纯纯的面向对象语言,函数并不能独立存在,只能依附于对象,函数只是某某某对象的一种行为,从面向对象的角度说是合理的没毛病的。

Java8后

千呼万唤始出来,Java8加入了Lambda 表达式,函数式接口,方法引用等特性支持函数式编程。

Lambda表达式使用

Lambda表达式就是Java支持函数式编程的体现

对于Android程序员Lambda表达式 最常见的情况 应该是设置按钮点击事件了。如下代码

  1. 先创建View.OnClickListener接口的匿名内部类
  2. 经过AndroidStudio提示后,使用快捷键把接口转换为Lambda表达式
  3. Lambda表达式赋值给变量
//匿名内部类
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

    }
});
//转换为Lambda表达式
button.setOnClickListener(view -> {

});
//Lambda表达式赋值给变量
View.OnClickListener listener = view -> {

};
button.setOnClickListener(listener);

借助开发工具的大力支持(哈哈哈)成功的编写了一个Lambda表达式

Lambda表达式解析

Lambda表示并不是一个对象,它对应的是接口中的抽象方法。抽象方法的参数,返回值决定了Lambda表达式改如何定义。

现在把IAddFunction 接口中的抽象方法改成View.OnClickListener 一致。则会出现如下情况,Lambda表达式一模一样,但是类型不一样。

View.OnClickListener listener = view -> { };
IAddFunction iAddFunction = view ->{ };

Java 8里面,所有的Lambda的类型都是一个接口,但Lambda表达式是接口中抽象方法的实现。 Lambda表达式不是类实例,是方法实现!!! 对应的是接口中的抽象方法,接口类型其实并不重要是猫是狗都可以。运用在函数式编程中的接口与面向对象中的接口是两回事

在函数式编程的世界里,函数不是某一个具体的类型的 比如:学生类,老师类。函数是一类类型,根据函数的参数,返回值类型确定的一类类型。 Java用接口作用Lambda表达式的类型感觉是没得办法的办法。

函数式接口与@FunctionalInterface注解

并不是所有的接口可以替换Lambda表达式

稍稍改动一下IAddFunction 新增一个抽象方法。修改完之后,原本的匿名内部类替换Lambda提示不见了。如果已经转换成Lambda表达式还会报错。

也很好理解,一个lambda表达式没办法同时对应两个抽象方法。

所以有且仅有一个抽象方法的接口才可以替换成Lambda表达式,这种接口被称为函数式接口。

也可以叫做“SAM InterfacesSingle Abstract Method Interfaces — 单一抽象方法接口

为了避免上述修改接口导致Lambda表达式出错的问题。Java 8 中专门为函数式接口引入了一个新的注解: @FunctionalInterface

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。

内置函数式接口

Lambda表达式的使用需要函数式接口。其实只需要确定抽象方法的参数类型和返回值类型即可,通用性很强。

所以JDK内置了很多函数式接口。在java.util.function

有四种类型的接口

  1. Function 转换数据:根据 一个类型的数据 得到另一个类型得数据
  2. Supplier 提供数据:获取一个值,无参数
  3. Consumer 处理数据:传入一个参数 无返回值
  4. Predicate 判断数据:传入一个参数 返回Boolean

还有很多其他接口 是基于上述接口实现的 比如:LongToIntFunction的作用是 把Long转为Int。

上述接口还有默认实现,就不一一详述了。

public interface Function<T, R> {
	R apply(T t);
}

public interface Supplier<T> {
    T get();
}

public interface Consumer<T> {
    void accept(T t);
}

public interface Predicate<T> {
    boolean test(T t);
}

方法引用

视线拉回IAddFunction 定义了加法的求和函数。

/**
 *加法函数
*/
interface IAddFunction {
    int add(int a, int b);
}

在方法调用处替换为Lambda表达式后,

AndroidStudio又提示 **“Lambda can be replaced with method reference”。**求和的Lambda表达式可以替换成一个方法引用。

替换结果为 Integer::sum点击进入源码查看是在Integer类定义的静态方法sum。内部实现求和算法

//Lambda表达式调用
test((a, b) -> a + b);
//方法引用调用
test(Integer::sum);

//Integer类的sum
public static int sum(int a, int b) {
    return a + b;
}

Lambda表达式实质上是一个方法实现,如果Lambda中逻辑的已经在别处实现过了,那就不用二次实现直接引用就好。所以出现了方法引用

方法引用结合JDK内置函数式接口 完美搭配。如下代码:

BiFunction<Integer, Integer, Integer> biFunction = Integer::sum;
int sum = biFunction.apply(12, 12);

方法引用的语法大概是类名::方法名 更具体用法就不再详述了。

很遗憾的一点是方法引用,JDK内置函数式接口在Android中需要sdk24(Android7.0)以后才支持,版本有点高了。这也是Android抛弃Java,拥抱kotlin的原因吧,很多Java的新特性都没办法用。

2.函数式编程与Kotlin

【码上开学】Kotlin 的高阶函数、匿名函数和 Lambda 表达式 - 掘金 (juejin.cn)

高阶函数与 lambda 表达式 - Kotlin 语言中文站 (kotlincn.net)

基本概念与使用

摘自官网:

Kotlin 函数都是头等的,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回。可以像操作任何其他非函数值一样操作函数。

关于函数式编程的理论上面已经聊很多了,从具体代码入手了解Kotlin的函数式编程

  1. 函数类型的语法:(参数,参数) -> 返回值
    1. () 括号中声明参数, 无参可以不写
    2. -> 箭头起分隔作用。分隔参数与返回值
    3. 返回值类型
  2. 定义测试方法test(),接受一个无参数无返回值的函数作为参数。将函数用作参数或返回值的函数叫做高阶函数
  3. 在函数中调用函数参数有两种方式
    1. 参数名()
    2. 参数名.invoke()
  4. 调用test方法需要传递对应类型的函数对象,有几种方式:
    1. lambda表达式
    2. 匿名函数
    3. 方法引用
    4. 函数类型实例对象

代码如下:

private fun setUp() {
    //方法引用
    test(::add)
    //lambda
    test{a, b->a + b}
     //匿名函数 1
    test(fun(a, b): Int {
        return a + b
    })
    //匿名函数2 简化写法
    test(fun(a, b) = a + b)
		//匿名函数3 完整写法
    val add: (a: Int, b: Int) -> Int = fun(a: Int, b: Int): Int {
       return a + b
    }
    test(add)
    //函数类型实例对象
    test(FuncImpl())
}

/**
 *高阶函数
*/
private fun test(helloWorld: (a: Int, b: Int) -> Int) {
    helloWorld.invoke(2, 3)
}

/**
 *求和函数实现
*/
private fun add(a: Int, b: Int): Int {
    Log.d("TAG", "hello world")
    return a + b
}

/**
 *函数类型实现类
*/
class FuncImpl : (Int, Int) -> Int {
    override fun invoke(p1: Int, p2: Int): Int {
        return p1 + p2
    }

}

摘自官方:

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。 即那些在函数体内会访问到的变量。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。

也就是说当Kotlin的函数当他们被变量存储,参数传递时已经变成了函数类型的对象而不是函数

官方在描述上述例子:lambda,匿名函数,方法应用的用词时函数类型的实例。包括在使用函数类型实现类时完全时创建了一个对象。也可以说明每一个函数都是一个对象。

内联函数

高阶函数效率低,可以通过内联函数优化。它的使用也很简单,只需要在高阶函数上新增一个关键字

inline 如下:

/**
 *调用高阶函数
*/
private fun setUp() {
   test { a, b -> a + b }
}

/**
 *高阶函数
*/
private inline fun test(helloWorld: (a: Int, b: Int) -> Int) {
    helloWorld.invoke(2, 3)
}

直接看编译后kotlin字节码的比对效果。

inline修饰的方法中的代码会直接插入到调用处。没有inline修饰会调用正常调用方法,生成函数对象

如果连续调用多个高阶函数,就会瞬间生成多个对象。 使用inline可以减少方法调用栈,但是延长编译时间,如果代码行数过多会增加代码量。所以inline就是搭配高阶函数使用的。

//使用inline
private final void setUp() {
   int $i$f$test = false;
   int b = 3;
   int a = 2;
   int var5 = false;
   int var10000 = a + b;
}

//未使用inline
private final void setUp() {
   this.test((Function2)null.INSTANCE);
}

对比Kotlin和Java的Lambda表达式语法

Java

(参数) -> {

	代码体;

}

BiFunction<Integer, Integer, Integer> biFunction = (a, b) -> {
     int c = a + b;
     return c;
};
  1. -> 箭头分隔参数和方法体
  2. 多个参数用逗号分隔
  3. 无参括号为空不可以不写
  4. 花括号表示方法体
  5. 返回值必须使用 return语句

Kotlin

{参数,参数->代码体
	
}

private val lambda = {a:Int,b:Int->
   val c = a+b
	 c
}
  1. 花括号表示lambda表达式
  2. 箭头分隔参数和方法体
  3. 多个参数用逗号分隔
  4. 无参可省略箭头 只写花括号
  5. 返回值可以使用return语句也可以不写,最后一行代码最为当前lambda的返回值。

Lambda表达式的简化写法

规则如下:

  1. 如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外
  2. 如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略
  3. 如果只有一个参数也可以省略不写,该参数会隐式声明为 it
//原始写法
mBinding.tvButton.setOnClickListener(object :View.OnClickListener{
    override fun onClick(p0: View?) {

    }
})
//转为lambda
mBinding.tvButton.setOnClickListener({view->{

}})
//只有一个参数 省略
mBinding.tvButton.setOnClickListener({

})
//lambda 是setOnClickListener() 方法参数中的最后一个
//lambda 挪到()后
mBinding.tvButton.setOnClickListener(){

}
//setOnClickListener() 只有一个参数 括号省略
mBinding.tvButton.setOnClickListener{

}