博客主页java
高阶函数就是以另外一个函数做为参数或者返回值的函数。在kotlin中,函数能够用lambda或函数引用来表示。
例如:标准库中的filter函数将一个判断式函数做为参数,因此就是一个高阶函数数据库
list.filter { x > 0 }
为了声明一个以lambda做为实参的函数,须要知道如何声明对应的形参的类型。编程
先来看一个简单的例子:把lambda表达式保存在局部变量中。segmentfault
// 有两个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是不能省略的。app
在lambda表达式{ x, y -> x + y }中是如何省略参数x,y的类型的呢?由于它们的类型已经在函数类型的变量声明中指定了,就不须要在lambda定义中重复声明。ide
函数类型的返回值也能够标记为可空类型:函数
var canReturnNull: (Int, Int) -> Int? = { null }
也能够定义一个函数类型的可空变量:post
var funOrNull: ((Int, Int) -> Int)? = null
知道了怎么声明高阶函数,那如何去实现呢?性能
举一个例子:定一个简单的高阶函数,实现两个数字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
背后的原理是:函数类型被声明为普通的接口,一个函数类型的变量是FunctionN接口的一个实现。
在kotlin标准库定义了一系列的接口,这些接口对应于不一样参数量的函数,Function0<R>(没有参数的函数),Function1<P1, R>(一个参数的函数)等。每一个接口定义了一个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<Integer, Integer>() { @Override public Integer invoke(Integer integer) { // 在java代码中使用函数类型(java8以前) return integer + 1; } });
在java中使用kotlin标准库中以lambda做为参数的函数,必须显式的传递一个接受者对象做为第一参数:
List<String> list = new ArrayList<>(); list.add("23"); // 能够在java中使用kotlin标准库中的函数 CollectionsKt.forEach(list, s -> { System.out.println(s); // 必须显式的返回一个Unit类型的值 return Unit.INSTANCE; });
举一个例子:使用了硬编码toString转换的joinToString函数
fun <T> Collection<T>.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 <T> Collection<T>.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 <T> Collection<T>.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() }
定义一个返回函数的函数:
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
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<SiteVisit>.averageDurationFor(os: OS) = filter { it.os == os }.map(SiteVisit::duration).average() println(list.averageDurationFor(OS.ANDROID)) // 16.3
用一个高阶函数去除重复代码:
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) = filter(predicate).map(SiteVisit::duration).average() println(list.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) }) // 19.15
lambda表达式会被正常地编译成匿名类。这表示每调用一次lambda表达式,一个额外的类就会被建立,这会带来运行时额外开销。
有没有可能让编译器生成跟java语句一样高效的代码,还能把重复的逻辑抽取到库函数中呢?kotlin的编译器可以作到。若是使用 inline 修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的饿真实代码替换每一次的函数调用。
当一个函数被声明为inline时,它的函数体是内联的,函数体会被直接替换到函数被调用的地方,而不是被正常调用。
// 定义一个内联函数 inline fun <T> 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() } } }
接下来看看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对内联函数的支持,咱们能够没必要担忧性能的问题。
使用inline关键字只能提升带有lambda参数的函数的性能,其它的状况须要额外的研究。
将带有lambda参数的函数内联,可以避免运行时开销。其实不只节约了函数调用的开销,并且节约了为lambda建立匿名类,以及建立lambda实例对象的开销。
在使用inline关键字的时,注意代码的长度。若是内联函数很大,将它的字节码拷贝到每个调用点将会极大地增长字节码的长度,应该将与lambda参数无关的代码抽取到一个独立的非内联函数中。
lambda能够去除重复代码的一个常见模式是资源管理:先获取一个资源,完成一个操做,而后释放资源。资源能够是一个文件,一个锁,一个数据库事务等。模式的标准作法是使用try/finally语句。
如前面实现的synchronized函数。但kotlin标准库定义了另外一个叫做withLock函数,它是Lock接口的扩展函数。
val lock : Lock = ReentrantLock() // 在加锁的状况下执行指定的操做 lock.withLock { // ... } // 这是Kotlin库中withLock函数的定义: // 须要加锁的代码抽取到一个独立的方法中 public inline fun <T> 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函数也是内联函数,不会引起任何性能开销。
来比较两种不一样的遍历集合的方法:
data class Person( val name: String, val age: Int ) val persons = listOf(Person("Alice", 23), Person("Bob", 43)) lookForAlice(persons) fun lookForAlice(persons: List<Person>) { for (person in persons) { if (person.name == "Alice") { println("Found!") return } } println("Alice is not Found!") } // Found!
若是使用forEach迭代重写这段代码安全吗?使用forEach也是安全的。
fun lookForAlice(persons: List<Person>) { 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表达式是不容许的。
也能够在lambda表达式中使用局部返回。lambda中的局部返回跟for循环中的break表达式类似。它会终止lambda的执行,并接着从调用lambda的代码处执行。
要区分局部返回仍是非局部返回,要用到标签。若是想从一个lambda表达式处返回,能够标记它,而后在return关键字后面引用这个标签。
// 用一个标签实现局部返回 fun lookForAlice(persons: List<Person>) { // 在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<Person>) { persons.forEach { if (it.name == "Alice") { // return@forEach从lambda表达式返回 return@forEach } } println("Alice is not Found!") }
// 在匿名函数中使用return fun lookForAlice(persons: List<Person>) { // 使用匿名函数取代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声明的函数返回。
若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)