java lambda 高阶函数_Kotlin核心语法(六):高阶函数,Lambda作为形参和返回值

1. 声明高阶函数

高阶函数就是以另一个函数作为参数或者返回值的函数。在kotlin中,函数可以用lambda或函数引用来表示。

例如:标准库中的filter函数将一个判断式函数作为参数,所以就是一个高阶函数

list.filter { x > 0 }

1.1 函数类型

为了声明一个以lambda作为实参的函数,需要知道如何声明对应的形参的类型。

先来看一个简单的例子:把lambda表达式保存在局部变量中。

// 有两个Int类型参数和Int类型的返回值的函数

val sum = {x: Int, y: Int -> x + y}

// 没有参数和返回值的函数

val action = { println("32") }

编译器会推导sum和action这两个变量具有函数类型。这些变量的显式类型声明是什么呢?

val sum: (Int, Int) -> Int = { x, y -> x + y }

val action: () -> Unit = { println("32") }

声明函数类型,需要将函数参数类型放在括号中,紧接着是一个箭头和函数返回类型。一个函数类型声明总是需要一个显式的返回类型,在这里Unit是不能省略的。

在lambda表达式{ x, y -> x + y }中是如何省略参数x,y的类型的呢?因为它们的类型已经在函数类型的变量声明中指定了,就不需要在lambda定义中重复声明。

函数类型的返回值也可以标记为可空类型:

var canReturnNull: (Int, Int) -> Int? = { null }

也可以定义一个函数类型的可空变量:

var funOrNull: ((Int, Int) -> Int)? = null

1.2 调用作为参数的函数

知道了怎么声明高阶函数,那如何去实现呢?

举一个例子:定一个简单的高阶函数,实现两个数字2和3的任意操作,然后打印结果

fun twoAndThree(operation: (Int, Int) -> Int) {

// 调用函数类型的参数

val result = operation(2, 3)

println("The result is $result")

}

twoAndThree { i, j -> i + j }

// The result is 5

twoAndThree { i, j -> i * j }

// The result is 6

调用作为参数的函数和调用普通函数的语法是一样的:把括号放在函数名后,并把参数放在括号中。

再举一个例子:实现一个简单版本的filter函数

其中filter函数是一个判断式作为参数。判断式的类型是一个函数,以字符作为参数并返回boolean类型的值。

fun String.filter(predicate: (Char) -> Boolean): String {

val sb = StringBuilder()

for (index in 0 until length) {

val element = get(index)

// 调用作为参数传递给"predicate"函数

if (predicate(element)) sb.append(element)

}

return sb.toString()

}

// 传递一个lambda,作为predicate参数

println("ab3d".filter { c -> c in 'a'..'z' })

// abd

2ddb50013489a38ebb932333dc1daef9.png

1.3 在java中使用函数类

背后的原理是:函数类型被声明为普通的接口,一个函数类型的变量是FunctionN接口的一个实现。

在kotlin标准库定义了一系列的接口,这些接口对应于不同参数量的函数,Function0(没有参数的函数),Function1(一个参数的函数)等。每个接口定义了一个invoke方法,调用这个方法就是执行函数。一个函数类型的变量就是实现了对应的FunctionN接口的实现类的实例,实现类的invoke方法包含了lambda函数体。

java8的lambda会被自动转换为函数类型的值

// kotlin 声明

fun processTheAnswer(f: (Int) -> Int) {

println(f(34))

}

// java

processTheAnswer.(number -> number + 1);

// 35

在旧版的java中,可以传递一个实现了接口函数中的invoke方法的匿名类的实例:

processTheAnswer(new Function1() {

@Override

public Integer invoke(Integer integer) {

// 在java代码中使用函数类型(java8之前)

return integer + 1;

}

});

在java中使用kotlin标准库中以lambda作为参数的函数,必须显式的传递一个接受者对象作为第一参数:

List list = new ArrayList<>();

list.add("23");

// 可以在java中使用kotlin标准库中的函数

CollectionsKt.forEach(list, s -> {

System.out.println(s);

// 必须显式的返回一个Unit类型的值

return Unit.INSTANCE;

});

1.4 函数类型的参数默认值和null值

举一个例子:使用了硬编码toString转换的joinToString函数

fun Collection.joinToString(

separator: String = ", ",

prefix: String = "",

postfix: String = ""

): String {

val result = StringBuilder(prefix)

for ((index, element) in this.withIndex()) {

if (index > 0) result.append(separator)

// 使用默认的toString方法将对象转换为字符串

result.append(element)

}

result.append(postfix)

return result.toString()

}

将集合中的元素转换为字符串,总是使用toString方法。可以定义一个函数类型的参数并用一个lambda作为它的默认值

fun Collection.joinToString(

separator: String = ", ",

prefix: String = "",

postfix: String = "",

// 声明一个以lambda为默认值的函数类型的参数

transform: (T) -> String = { it.toString() }

): String {

val result = StringBuilder(prefix)

for ((index, element) in this.withIndex()) {

if (index > 0) result.append(separator)

// 调用作为实参传递给 "transform" 形参的函数

result.append(transform(element))

}

result.append(postfix)

return result.toString()

}

val list = listOf("A", "B", "C")

// 传递一个lambda作为参数

println(list.joinToString { it.toLowerCase() })

// a, b, c

还可以声明一个参数为可空的函数类型。但是不能直接调用作为参数传递进来的函数,kotlin会因为检测到潜在的空指针异常而导致编译失败。我们可以显式地检查null

fun foo(callback: (() -> Unit)?) {

// ...

if (callback != null) {

callback()

}

}

可以利用函数类型是一个包含invoke方法的接口的具体实现。

举一个例子:使用函数类型的可空参数

fun Collection.joinToString(

separator: String = ", ",

prefix: String = "",

postfix: String = "",

// 声明一个函数类型的可空参数

transform: ((T) -> String)? = null

): String {

val result = StringBuilder(prefix)

for ((index, element) in this.withIndex()) {

if (index > 0) result.append(separator)

// 使用安全调用语法调用函数

// 使用Elvis运算符处理回调没有被指定的情况

val str = transform?.invoke(element) ?: element.toString()

result.append(str)

}

result.append(postfix)

return result.toString()

}

1.5 返回函数的函数

定义一个返回函数的函数:

enum class Delivery {

STANDARD, EXPEDITED

}

class Order(val itemCount: Int)

fun getShippingCostCalculator(

delivery: Delivery

): (Order) -> Double { // 声明一个返回函数的函数

if (delivery == Delivery.EXPEDITED) {

// 返回lambda

return { order -> 6 + 2.1 * order.itemCount }

}

return { order -> 1.2 * order.itemCount }

}

// 将返回的函数保存在变量中

val calculator = getShippingCostCalculator(Delivery.EXPEDITED)

// 调用返回的函数

println("shipping costs ${calculator(Order(3))}")

// shipping costs 12.3

1.6 通过lambda去除重复代码

data class SiteVisit(

val path: String,

val duration: Double,

val os: OS

)

enum class OS {

IOS, ANDROID

}

首先使用硬编码的过滤器分析数据:

val list = listOf(

SiteVisit("/", 22.0, OS.IOS),

SiteVisit("/", 16.3, OS.ANDROID)

)

val averageIOSDuration = list

.filter { it.os == OS.IOS }

.map(SiteVisit::duration)

.average()

println(averageIOSDuration)

// 22.0

假设需要分析ANDROID数据,为了避免重复,可以抽象一个参数。

用一个普通方法去除重复代码:

// 将重复代码抽取到函数中

fun List.averageDurationFor(os: OS) =

filter { it.os == os }.map(SiteVisit::duration).average()

println(list.averageDurationFor(OS.ANDROID))

// 16.3

用一个高阶函数去除重复代码:

fun List.averageDurationFor(predicate: (SiteVisit) -> Boolean) =

filter(predicate).map(SiteVisit::duration).average()

println(list.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) })

// 19.15

2. 内联函数:消除lambda带来的运行时开销

lambda表达式会被正常地编译成匿名类。这表示每调用一次lambda表达式,一个额外的类就会被创建,这会带来运行时额外开销。

有没有可能让编译器生成跟java语句同样高效的代码,还能把重复的逻辑抽取到库函数中呢?kotlin的编译器能够做到。如果使用 inline 修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的饿真实代码替换每一次的函数调用。

2.1 内联函数如何运作

当一个函数被声明为inline时,它的函数体是内联的,函数体会被直接替换到函数被调用的地方,而不是被正常调用。

// 定义一个内联函数

inline fun synchronized(lock: Lock, action: () -> T): T {

lock.lock()

try {

return action()

} finally {

lock.unlock()

}

}

val lock = ReentrantLock()

synchronized(lock) {

// ...

}

因为已经将synchronized函数声明为inline,所以每次调用它所生成的代码跟java的synchronized语句是一样的。

fun foo(lock: Lock) {

println("Before sync")

synchronized(lock) {

println("Action")

}

println("After sync")

}

这段代码编译后的foo函数:

fun foo(lock: Lock) {

println("Before sync")

// 被内联的synchronized函数代码

lock.lock()

try {

println("Action") // 被内联的lambda体代码

} finally {

lock.unlock()

}

println("After sync")

}

由lambda生成的字节码成为了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中。

在调用内联函数的时,也可以传递函数类型的变量作为参数:

class LockOwner(

val lock: Lock

) {

fun runUnderLock(body: () -> Unit) {

// 传递一个函数类型的变量作为参数,而不是一个lambda

synchronized(lock, body)

}

}

在这种情况下,lambda的代码在内联函数被调用点是不可用的,因此并不会被内联。只有synchronized的函数体被内联了,lambda才会被正常调用。

runUnderLock函数会被编译成类似于以下函数的字节码:

class LockOwner(

val lock: Lock

) {

// 这个函数类似于真正的runUnderLock被编程成的字节码

fun runUnderLock(body: () -> Unit) {

lock.lock()

try {

// body没有被内联,因为在调用的地方还没有lambda

body()

} finally {

lock.unlock()

}

}

}

2.2 内联集合操作

接下来看看kotlin标准库操作集合的函数性能。

举一个例子:使用lambda过滤一个集合

data class Person(

val name: String,

val age: Int

)

val persons = listOf(Person("Alice", 23), Person("Bob", 43))

println(persons.filter { it.age < 30 })

// [Person(name=Alice, age=23)]

在kotlin中,filter函数被声明为内联函数。意味者filter函数,以及传递给它的lambda的字节码会被一起内联到filter被调用的地方。kotlin对内联函数的支持,我们可以不必担心性能的问题。

2.3 决定何时将函数声明成内联

使用inline关键字只能提高带有lambda参数的函数的性能,其它的情况需要额外的研究。

将带有lambda参数的函数内联,能够避免运行时开销。其实不仅节约了函数调用的开销,而且节约了为lambda创建匿名类,以及创建lambda实例对象的开销。

在使用inline关键字的时,注意代码的长度。如果内联函数很大,将它的字节码拷贝到每一个调用点将会极大地增加字节码的长度,应该将与lambda参数无关的代码抽取到一个独立的非内联函数中。

2.4 使用内联lambda管理资源

lambda可以去除重复代码的一个常见模式是资源管理:先获取一个资源,完成一个操作,然后释放资源。资源可以是一个文件,一个锁,一个数据库事务等。模式的标准做法是使用try/finally语句。

如前面实现的synchronized函数。但kotlin标准库定义了另一个叫作withLock函数,它是Lock接口的扩展函数。

val lock : Lock = ReentrantLock()

// 在加锁的情况下执行指定的操作

lock.withLock { // ... }

// 这是Kotlin库中withLock函数的定义:

// 需要加锁的代码抽取到一个独立的方法中

public inline fun Lock.withLock(action: () -> T): T {

lock()

try {

return action()

} finally {

unlock()

}

}

在java中使用try-with-resource语句:

static String readFirstLineFromFile(String path) throws IOException {

try (BufferedReader br = new BufferedReader(new FileReader(path))) {

return br.readLine();

}

}

在kotlin标准库中可以使用use函数:

fun readFirstLineFromFile(path: String): String {

// 构建BufferedReader,调用use函数,传递一个lambda执行文件操作

BufferedReader(FileReader(path)).use { br ->

return br.readLine()

}

}

use函数是一个扩展函数,被用来操作可关闭的资源,它接收一个lambda作为参数。use函数也是内联函数,不会引发任何性能开销。

3. 高阶函数中的控制流

3.1 lambda中的返回语句:从一个封闭的函数返回

来比较两种不同的遍历集合的方法:

data class Person(

val name: String,

val age: Int

)

val persons = listOf(Person("Alice", 23), Person("Bob", 43))

lookForAlice(persons)

fun lookForAlice(persons: List) {

for (person in persons) {

if (person.name == "Alice") {

println("Found!")

return

}

}

println("Alice is not Found!")

}

// Found!

如果使用forEach迭代重写这段代码安全吗?使用forEach也是安全的。

fun lookForAlice(persons: List) {

persons.forEach {

if (it.name == "Alice") {

println("Found!")

return

}

}

println("Alice is not Found!")

}

// Found!

如果在lambda中使用return关键字,它会从调用lambda的函数中返回,并不只是从lambda中返回。这样的return语句叫作非局部返回,因为它从一个比包含return的代码块更大的代码块中返回了。

只有在以lambda作为参数的函数是内联函数的时候才能从更外层的函数返回。forEach的函数体和lambda的函数体一起被内联了,所以在编译的时候很容易做到从包含它的函数中返回。在一个非内联函数的lambda中使用return表达式是不允许的。

3.2 从lambda返回:使用标签返回

也可以在lambda表达式中使用局部返回。lambda中的局部返回跟for循环中的break表达式相似。它会终止lambda的执行,并接着从调用lambda的代码处执行。

要区分局部返回还是非局部返回,要用到标签。如果想从一个lambda表达式处返回,可以标记它,然后在return关键字后面引用这个标签。

// 用一个标签实现局部返回

fun lookForAlice(persons: List) {

// 在lambda表达式上加标签

persons.forEach label@{

if (it.name == "Alice") {

// return@label引用了这个标签

return@label

}

}

println("Alice is not Found!")

}

要标记一个lambda表达式,在lambda的花括号之前放一个标签名(可以是任何标识符),接着跟一个@符号。要从一个lambda返回,在return关键字后跟一个@符号,接着跟标签名。

使用lambda作为参数的函数的函数名可以作为标签:

// 用函数名作为return标签

fun lookForAlice(persons: List) {

persons.forEach {

if (it.name == "Alice") {

// return@forEach从lambda表达式返回

return@forEach

}

}

println("Alice is not Found!")

}

3.3 匿名函数:默认使用局部返回

// 在匿名函数中使用return

fun lookForAlice(persons: List) {

// 使用匿名函数取代lambda表达式

persons.forEach(fun(person) {

// return指向最近的函数:一个匿名函数

if (person.name == "Alice") return

println("${person.name} is not Alice!")

})

}

// Bob is not Alice!

匿名函数省略了函数名和参数类型。

// 在filter中使用匿名函数

persons.filter(fun(person): Boolean {

return person.age < 30

})

如果使用表达式函数体,就可以省略返回值类型

// 使用表达式函数体匿名函数

persons.filter(fun(person) = person.age < 30)

在匿名函数中,不带标签的return表达式会从匿名函数返回,而不是从包含匿名函数的函数返回。return从最近的使用fun关键字声明的函数返回。

lambda表达式没有使用fun关键字,所以lambda中的return从最外层的函数返回。匿名函数使用了fun关键字,是从最近fun声明的函数返回。

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值