0.前言
函数式编程:
- 是一种区别与面向对象的开发方式
- 面向对象针对于一个业务实体 或 一组业务实体 进行抽象封装。业务实体的数据变为类属性,业务实体的行为变为类方法。在面向对象中函数(方法)并不能独立存在,必须依附于某个类才能被调用。面向对象倾向于组织系统结构
- 函数式编程是对计算的抽象,倾向于分隔问题,根据实际情况把代码划分为一个个函数。不需要属性,不需要new对象,不需要繁琐的语法,包含几行或十几行的代码块才是关键。
- 偏工具类的例子:字符串分隔的算法。
- 偏业务的例子:根据用户状态展示不同的图标
- 语言层面
- 函数也是一种类型,可以引用,当作参数传递,可以把一块代码赋值给变量
- 函数可以脱离类直接定义,使用,函数与类是平等关系
- 函数式编程和面向对象编程之间绝对不冲突。 你比我好,我比你强,一定要分个高下。这种想法是绝对错误的,它们是互补的。
- 构建一套企业办公系统,雇员,组长,经理,总监,老板。每个角色有不用的权限,不用的行为。很明显这活就得用面向对象来干。
- 如果系统中需要一个简单的发邮件功能。当然是直接调用一个函数更方便了。
- 函数式以好面向对象也罢,关键是把握它们的思想。 用java就一定写的是面向对象代码么?用js就没办法合理封装抽象业务实体么?难说呀 🐶
1.函数式编程与Java
Lambda 表达式有何用处?如何使用? - Mingqi的回答 - 知乎 www.zhihu.com/question/20…
☕️【Java技术之旅】带你看透Lambda表达式的本质_Java_浩宇天尚_InfoQ写作平台
Java8前
在Java8之前是不支持函数式编程的。从语法上说不存在函数类型,函数只能通过对象调用,没办法直接引一个函数,赋值给某个变量。
模拟一个简单需求,定义加法函数,传入参数a,b返回两数相加的和。Java8之前开发人员通过接口达到类似的效果。
代码如下:
- 定义接口IAddFunction
- 定义方法:
private void test(IAddFunction addFunction)参数是接口IAddFunction - test方法内部,模拟逻辑,调用
IAddFunction接口实现两数相加。 - onCreate方法中,通过创建匿名内部类 和 接口实现 的方式 向test方法传递引用。
- 如下实现虽然达到了 test方法中 调用 add方法 的效果。但是add()仍然是一个类方法。使用时必须通过对象调用方法,而不是直接调用方法
- 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表达式 最常见的情况 应该是设置按钮点击事件了。如下代码
- 先创建View.OnClickListener接口的匿名内部类
- 经过AndroidStudio提示后,使用快捷键把接口转换为Lambda表达式
- 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 Interfaces—Single Abstract Method Interfaces — 单一抽象方法接口”
为了避免上述修改接口导致Lambda表达式出错的问题。Java 8 中专门为函数式接口引入了一个新的注解: @FunctionalInterface
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。
内置函数式接口
Lambda表达式的使用需要函数式接口。其实只需要确定抽象方法的参数类型和返回值类型即可,通用性很强。
所以JDK内置了很多函数式接口。在java.util.function
有四种类型的接口
- Function 转换数据:根据 一个类型的数据 得到另一个类型得数据
- Supplier 提供数据:获取一个值,无参数
- Consumer 处理数据:传入一个参数 无返回值
- 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的函数式编程
- 函数类型的语法:(参数,参数) -> 返回值
- () 括号中声明参数, 无参可以不写
- -> 箭头起分隔作用。分隔参数与返回值
- 返回值类型
- 定义测试方法test(),接受一个无参数无返回值的函数作为参数。将函数用作参数或返回值的函数叫做高阶函数
- 在函数中调用函数参数有两种方式
- 参数名()
- 参数名.invoke()
- 调用test方法需要传递对应类型的函数对象,有几种方式:
- lambda表达式
- 匿名函数
- 方法引用
- 函数类型实例对象
代码如下:
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;
};
- -> 箭头分隔参数和方法体
- 多个参数用逗号分隔
- 无参括号为空不可以不写
- 花括号表示方法体
- 返回值必须使用 return语句
Kotlin
{参数,参数->代码体
}
private val lambda = {a:Int,b:Int->
val c = a+b
c
}
- 花括号表示lambda表达式
- 箭头分隔参数和方法体
- 多个参数用逗号分隔
- 无参可省略箭头 只写花括号
- 返回值可以使用return语句也可以不写,最后一行代码最为当前lambda的返回值。
Lambda表达式的简化写法
规则如下:
- 如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外
- 如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略
- 如果只有一个参数也可以省略不写,该参数会隐式声明为
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{
}