kotlin-详解高阶函数、函数类型、lambda、成员引用

468 阅读9分钟

详解高阶函数、函数类型、lambda、成员引用

什么是高阶函数?

在 kotlin 中高阶函数指以另一个函数作为参数或返回值的函数。

通过上面高阶函数的定义我们知道高阶函数可以接收函数作为它的实参,可以将函数作为它的返回值。在 java 中我们知道不论是参数还是返回值都有其对应的类型,如 String、int、boolean。那么 kotlin 中的高阶函数的参数或返回值又是什么类型呐?上面说函数可以作为高阶函数的参数或返回值,难道在 kotlin 中存在一种类型叫函数类型?没错 kotlin 中的确存在一种类型叫函数类型(至于什么是函数类型我们稍后介绍)。

其实高阶函数更准确的定义如下: 参数类型或返回值类型是函数类型的函数即高阶函数。

kotlin 中的函数类型是什么类型?

函数类型是某一类函数的抽象表达,一个函数就是一个函数类型的实例,就是一个函数类型的对象。 那如何表示函数类型呐? 在了解如何表示函数类型之前,我们先来看一下一个函数由哪些部分组成。

String function(String param) {
    String prefix = "pre";
    String result = prefix + param;
    return result;
}

我们可以看出一个函数由 返回值类型、函数名、参数类型、参数名以及函数体组成。既然函数类型是某一类函数的抽象表达那么我们对以上函数进行抽象看最终哪些部分能共用?函数体是函数的实现部分,每个函数的函数体都应该是独一无二的,没办法共用;函数名作为函数的标识用以区分参数类型相同的函数,也没办法共用;参数名用于在函数体中区分各个参数,但是函数体不能共用,共用参数名也就没有意义了。 经过上面的分析我们发现最终剩下了 参数类型和返回值类型,kotlin 中的函数类型正是由参数类型和返回值类型组成的。 在 kotlin 中声明函数类型需要将函数的参数类型放在括号中,紧接着是一个箭头和函数的返回值类型。 (参数类型)->返回值类型 (Int,String) -> String 以上函数类型表示 第一个参数是 Int 类型,第二个参数是 String 类型,返回值为 String 类型的函数。

// 在 kotlin 中 val 用以声明变量
// 在 kotlin 变量名在 : 左侧,变量类型在 : 右侧
val sum: (Int, Int) -> Int 
// 以上表示声明一个 第一个参数是 Int 类型,第二个参数是 Int 类型,返回值是 Int 类型的函数类型变量 sum

注意:

  • 函数类型的返回值类型可以声明为可空类型val canReturnNull: (Int, Int) -> Int?

  • 声明一个可空的函数类型 val funOrNull: ((Int, Int) -> Int)?

  • 为函数类型中声明的参数指定名字 val sum: (age:Int, name:String) -> Int

kotlin 中的函数类型对应 java 中的什么?

kotlin 的代码最终都会编译成 .class 那么 kotlin 中的函数类型对应 java 中的什么呐?

class Height {
    val sum: (Int, Int) -> Int = { a, b -> a * b }
}

如上声明一个函数类型的变量,通过 idea 提供的 Show Kotlin Bytecode 反编译查看其对应的 java 代码如下

import kotlin.Metadata;
import kotlin.jvm.functions.Function2;
import org.jetbrains.annotations.NotNull;
​
public final class Height {
   @NotNull
   private final Function2 sum;
​
   @NotNull
   public final Function2 getSum() {
      return this.sum;
   }
​
   public Height() {
      this.sum = (Function2)null.INSTANCE;
   }
}

可以看出我们声明的函数类型在 java 中变成了 Function2,那 Function2 又是什么如何定义的呐?

/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}
/** A function that takes 2 arguments. */
public interface Function2<in P1, in P2, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2): R
}
​
......
​
/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}

查看 Function2 的定义可以发现 kotlin 中定义了从 Function0 到 Function22,23 个泛型接口。从注释中可以发现泛型接口的形参对应其抽象方法的参数类型和返回值类型。 kotlin 中声明的函数类型在 java 中被声明为函数式接口,一个函数类型的变量是 FunctionN 接口的一个实现。 kotlin 标准库中定义了一系列的接口,这些接口对应于不同参数数量的函数:Function0(没有参数的函数,R 为返回值类型)、Function1<P1,R>(一个参数的函数,P1 为参数类型,R 为返回值类型)。每个接口定义了一个invoke方法,调用这个方法就会执行函数。一个函数类型的变量就是实现了对应的 FunctionN 接口的实现类的实例,实现类的 invoke 方法的方法体对应 Lambda 表达式体。 其实 一个函数类型的实例 或 函数类型的对象 就是一个函数式接口的实例。

函数类型的对象如何使用?

了解了什么是高阶函数、函数类型是什么,但是我们还没有看到一个具体的高阶函数,下面我们就来认识一下。

class Height {
    fun invokeSum(a: Int, b: Int, operation: (Int, Int) -> Int) {
        operation(a, b)
    }
}

这个高阶函数接收三个参数,前两个参数都是 Int 类型的数值,第三个参数是一个 参数是两个 Int 类型,返回值也是 Int 类型的函数类型。这个高阶函数实现的功能是根据 函数类型的实参的实现 实现两个数值类型的运算。 从上面 函数类型的参数的使用可以看出 一个函数类型的对象可以当作成一个函数使用,通过 变量名(参数、参数)的方式调用。 上一小节也提到 函数类型 在 java 中被声明为函数式接口,那上面的高阶函数转化为 java 又是什么样的呐?

import kotlin.Metadata;
import kotlin.jvm.functions.Function2;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
​
public final class Height {
   public final void invokeSum(int a, int b, @NotNull Function2 operation) {
      Intrinsics.checkNotNullParameter(operation, "operation");
      operation.invoke(a, b);
   }
}

可以看到函数类型 (Int, Int) -> Int 转换成了 Function2,并且通过 变量名.invoke(参数、参数)的方式进行调用,这种方式调用在 kotlin 中也可以使用,尤其是当 函数类型 是一个可空类型时,通过 变量名?.invoke(参数、参数)进行安全调用可防止空指针异常。 综上函数类型的对象有两种调用方式:

  1. 变量名(参数、参数)
  2. 变量名?.invoke(参数、参数

高阶函数如何调用?

调用高阶函数的关键其实就是如何创建一个函数类型的对象。 高阶函数的调用又分为在 kotlin 中还是 java 中。

在 kotlin 中调用高阶函数

在 kotlin 中通常通过 lambda 表达式 和 成员引用 表示函数类型的对象。

lambda 表达式

在 kotlin 中 lambda 表达式、函数类型的对象、函数式接口的实例 可以相互替换。 kotlin 中的 Lambda 表达式始终用花括号包围,箭头把实参列表和 Lambda 表达式的函数体隔开。

val sum = { x: Int, y: Int -> x + y }
            参数     参数       函数体  

lambda 可以被独立地声明并存储到一个变量中,并把这个变量当做普通函数对待。

fun main() {
    val sum = { x: Int, y: Int -> x + y }
    println(sum(3,6))
}

lambda 可以被当作值传递即调用高阶函数时作为函数类型的实参,如下

fun main() {
    val persons:ArrayList<Person> = ArrayList()
    persons.maxBy({ person: Person -> person.age })
}

lambda 表达式的语法糖

  • Kotlin 中如果 Lambda 表达式是函数的最后一个实参,在调用此函数时可以将 Lambda 表达式放到括号外边。persons.maxBy(){ person: Person -> person.age }
  • 当 Lambda 表达式是函数的唯一实参,调用函数时可以省略其中的空括号。persons.maxBy{ person: Person -> person.age }
  • 如果 Lambda 表达式的参数类型可以被推导出来,不需要显示地指定它。persons.maxBy{ person -> person.age }
  • 如果 Lambda 表达式只有一个参数并且参数的类型可以推导出来,则可以省略 Lambda 表达式的参数部分,在 Lambda 的函数体部分以it代替此实参。persons.maxBy { it.age }
  • 当 Lambda 表达式中有多条语句时,最后一个表达式就是 Lambda 表达式的结果。val sum: (Int, Int) -> Int = { a, b -> a * b }

成员引用

成员引用是指引用定义好的成员函数可以利用成员引用将已经定义好的函数,作为实参传递给高阶函数 或者 赋值给函数类型的变量。成员引用使用 :: 运算符,成员引用运算符(::)把需要引用的成员所在的类的类与需要引用的成员的名称分隔开来。 成员引用主要是理解引用不同类型的函数时所对应的函数类型是什么!!!下面分别介绍

顶层函数的成员引用

fun topFun(a: Int): String {
    return a.toString()
}

val topFun: (Int) -> String = ::topFun

类函数的成员引用

class Height {
​
    fun classFun(a: Int): String {
        return a.toString()
    }
​
    val classFun: (Height, Int) -> String = Height::classFun
}

对象函数的成员引用

class Height {
​
    fun objFun(a: Int): String {
        return a.toString()
    }
​
    val objFun: (Int) -> String = this::objFun
}

构造函数的成员引用

class Height {
​
    constructor(name: String)
​
    val classFun: (String) -> Height = ::Height
}

在 java 中调用高阶函数

前面介绍到函数类型其实就是相当于 java 中的函数式接口,那么一个函数式接口的实例自然就是一个函数类型的对象,不过通过这种方式创建函数类型的对象在 kotlin 中并不常用,反而是在 java 中创建函数类型的对象的方法。

class Height {
    fun invokeSum(a: Int, b: Int, operation: (Int, Int) -> Int) {
        operation(a, b)
    }
}

Java 中调用高阶函数

public class J {
    public static void main(String[] args) {
        Height height = new Height();
        height.invokeSum(1, 3, new Function2<Integer, Integer, Integer>() {
            @Override
            public Integer invoke(Integer integer, Integer integer2) {
                return integer * integer2;
            }
        });
    }
}