目录
Lambda表达式
高阶函数
所谓高阶函数就是把函数当作参数传递或者返回值的函数。
函数类型的声明
我们来看一下实际的例子:
fun main() {
println(f(1, 2, { a, b -> a + b }))
println(f(1, 2, { a, b -> a - b }))
}
fun f(i: Int, j: Int, op: (Int, Int) -> Int): Int {
return op(i, j)
}
// output:3
// output:-2
这里的f函数是一个高阶函数,它有三个参数,一个是分别是i、j、和op,i和j都是Int类型,而op是一个函数类型,我们看下它的类型声明( Int, Int) -> Int
,括号里的Int
是函数类型声明的参数,-> Int
这里的Int指的是函数类型的返回值。那么这个函数参数的意义就是通过两个Int值的某些操作返回一个Int值。而这个所谓的某些操作就是我们要具体传递的方法。例如{ a, b -> a + b }
表示将两个Int值相加。{ a, b -> a - b }
表示将两个数相减。
所有的函数类型都会有一个括号括起来的参数类型列表和返回类型。比如(A, B) -> C
表示一个函数需要传递两个参数,类型分别是A和B,返回值类型是C。参数类型可以省略,但是返回值类型不能省略。如() -> Unit
。
函数类型可以额外有一个接受者类型。例如这个定义:A.(B) -> C
,表示这个函数可以被A类型调用,参数是B类型,返回值是C类型。在这个函数内部可以直接通过this
访问A类的属性或者方法,甚至直接省略this
。
函数类型也有几个特殊的例子:
(Int, Int) -> Int?
表示参数类型是Int
,返回值类型是Int?
((Int, Int) -> Int)?
表示参数类型是Int
,返回值类型是Int
,但是整个函数可以为null(Int) -> ((Int) -> Unit)
表示参数类型是Int
,返回值类型是另外一个函数类型,而这个函数类型的参数是Int
,返回值类型是Unit
(Int) -> (Int) -> Unit
的箭头优先级的顺序是从右向左,也就是它等价于(Int) -> ((Int) -> Unit)
函数类型的初始化
有以下几种方式获取一个函数类型的实例:
- 通过lambda表达式
{ a, b -> a + b }
- 通过一个匿名函数
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
- 通过一个引用已经存在的声明
String::toInt
- 如果类实现了将函数类型接口,可以使用这个类的实例,例如:
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
函数类型的引用
可以通过invoke方法调用,或者直接调用。举个例子:
fun main() {
val intPlus: (Int, Int) -> Int = { a, b -> a + b }
intPlus.invoke(2,3)
intPlus(4,5)
}
如果是带接受者的函数类型,可以把接受者作为第一个参数调用或者将接受者放在括号外类似于扩展函数的方式调用。例如:
import java.lang.StringBuilder
fun main() {
// 将string复制n份
val stringPlus: String.(Int) -> String = { it ->
var count = it
val builder = StringBuilder()
while (count-- > 0) {
builder.append(this)
}
builder.toString()
}
println("abs".stringPlus(3))
println(stringPlus("xyz",5))
println(stringPlus.invoke("mnp",4))
}
// output:
// absabsabs
// xyzxyzxyzxyzxyz
// mnpmnpmnpmnp
将Lambda表达式传递给java方法
我们知道java中的部分匿名内部类在kotlin中可以使用lambda来表示,那么lambda表达式对应的java code是不是就是一个匿名内部类呢?
我们来看几个例子:
假设我们定义了一个java 接口和设置这个接口的方法:
public interface OnClickListener {
void onClick();
}
public class HelloJava {
public void setListener(OnClickListener onClickListener){
}
}
现在在Kotlin中调用这个setListener
方法:
fun main() {
var s = "sss"
// 1
HelloJava().setListener { println("hi") }
// 2
HelloJava().setListener { println("hi") }
// 3
HelloJava().setListener { print("$s") }
// 4
HelloJava().setListener(object : OnClickListener {
override fun onClick() {
println("hi")
}
})
}
1、2、3都是使用lambda的方式,而4是使用匿名内部类的方法。
来看下这四种方法对应的java code:
public static final void main() {
final ObjectRef s = new ObjectRef();
s.element = "sss";
// 1
(new HelloJava()).setListener((OnClickListener)null.INSTANCE);
// 2
(new HelloJava()).setListener((OnClickListener)null.INSTANCE);
// 3
(new HelloJava()).setListener((OnClickListener)(new OnClickListener() {
public final void onClick() {
String var1 = String.valueOf((String)s.element);
System.out.print(var1);
}
}));
// 4
(new HelloJava()).setListener((OnClickListener)(new OnClickListener() {
public void onClick() {
String var1 = "hi";
System.out.println(var1);
}
}));
}
我们可以看到kotlin对应的java code中1、2两个是生成了两个INSTANCE
实例。而3、4生成的是匿名内部类。
现在有两个问题:
1.INSTANCE
实例对应的类是什么呢?
2.第一个lambda和第二个lambda表达式相同,那么它们生成的INSTANCE
对应的是同一个类甚至是同一个实例吗?
3.第三个lambda表达式为什么没有生成INSTANCE
实例呢?
首先我们来看下第一个lambda
中的相关kotlin bytecode代码:
L1
LINENUMBER 7 L1
NEW HelloJava
DUP
INVOKESPECIAL HelloJava.<init> ()V
GETSTATIC HelloKt$main$1.INSTANCE : LHelloKt$main$1; // 这里
CHECKCAST OnClickListener
INVOKEVIRTUAL HelloJava.setListener (LOnClickListener;)V
我们能看到INSTANCE
其实是HelloKt$main$1
的一个静态字段。
HelloKt$main$1
是一个单例,它的类名生成规则是类名$方法名$后缀
。
在阅读《Kotlin in Action》时,书中有这么一段话:
When you explicitly declare an object, a new instance is created on each invocation. With a lambda, the situation is different: if the lambda doesn’t access any variables from the function where it’s defined, the corresponding anonymous class instance is reused between calls.
当显式声明匿名内部类时,每次调用都会创建一个新实例。使用lambda表达式的时候是分情况的:如果lambda表达式没有引用范围外的变量,那么它创建的相应的匿名类会在不同调用之间重用。
最开始看这段话的时候,可能理解有些偏差。最开始的理解是:如果两次lambda内容相同,并且都没有引用外面的变量,那么这两个lambda会使用同一个类。
但是后来验证了以下,其实是错误的。
每次创建一个lambda都会创建一个新的类。类名的规则就是类名$方法名$后缀
,后缀是递增的。那么之前那段话里的reused
该怎么去理解呢?
这里的reused
其实是指的是使用同一个lambda表达式:
例如:
val function = { println("hi") }
HelloJava().setListener(function)
HelloJava().setListener(function)
这个代码确实会重用一个单例类。
如果lambda引用了范围外的变量,那么那还是会正常生成一个匿名内部类。引用的变量会通过一个包装类封装,因此在lambda中更改外部变量的值也是有效的。
inline
在上一小节可以看到lambda表达式会生成一个新的类,这样相比于直接执行相应的代码就会有性能上的不足。那么有没有办法既能高效执行代码,又可以将函数抽取出来呢?使用inline就可以。
当一个函数声明为inline的时候,那么函数体会直接替换到函数调用的地方。
来看个例子:
fun main() {
action(1, 2) { a, b -> a + b }
}
inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int {
return op.invoke(a, b)
}
我们吧action函数声明为inline,来看下它的java code:
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\u0016\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a3\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u00012\u0006\u0010\u0003\u001a\u00020\u00012\u0018\u0010\u0004\u001a\u0014\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u00010\u0005H\u0086\b\u001a\u0006\u0010\u0006\u001a\u00020\u0007¨\u0006\b"},
d2 = {"action", "", "a", "b", "op", "Lkotlin/Function2;", "main", "", "LearnKotlin"}
)
public final class HelloKt {
public static final void main() {
byte a$iv = 1;
int b$iv = 2;
int $i$f$action = false;
int var5 = false;
int var10000 = a$iv + b$iv;
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final int action(int a, int b, @NotNull Function2 op) {
int $i$f$action = 0;
Intrinsics.checkParameterIsNotNull(op, "op");
return ((Number)op.invoke(a, b)).intValue();
}
}
可以看到在main()
方法中,没有直接调用action方法,而是把action里的逻辑直接替换处理,这样代码执行效率更高。
作为对比,可以再来看下action不声明为inline时的java code:
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\u0016\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a0\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u00012\u0006\u0010\u0003\u001a\u00020\u00012\u0018\u0010\u0004\u001a\u0014\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u00010\u0005\u001a\u0006\u0010\u0006\u001a\u00020\u0007¨\u0006\b"},
d2 = {"action", "", "a", "b", "op", "Lkotlin/Function2;", "main", "", "LearnKotlin"}
)
public final class HelloKt {
public static final void main() {
action(1, 2, (Function2)null.INSTANCE);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final int action(int a, int b, @NotNull Function2 op) {
Intrinsics.checkParameterIsNotNull(op, "op");
return ((Number)op.invoke(a, b)).intValue();
}
}
noinline
但是不是所有使用lambda的函数都可以被声明为inline,如果我们的lambda表达式在函数中被保存了下来,那么是不能声明为inline的。举个例子:
fun main() {
action(1, 2, { i, j -> i + j })
}
inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int {
val lambda = op
return lambda.invoke(a, b)
}
这段代码是无法被编译通过的,回报如下错误:
Error:(10, 18) Kotlin: Illegal usage of inline-parameter ‘op’ in ‘public inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int defined in root package in file Hello.kt’. Add ‘noinline’ modifier to the parameter declaration
假设我们一个函数参数中不只有一个lambda,而有的参数不应该被声明成inline,那么可以将这个参数用noinline
修饰。如:
inline fun action(a: Int, b: Int,
op1: (Int, Int) -> Int, noinline op2: (Int, Int) -> Int) {
}
集合函数式API
filter:从现有集合中选出满足条件的元素
map:将每一个元素转化为新的元素
all:所有元素是否全部满足条件
any:是否有一个元素满足条件
count:满足条件的元素的个数
find:找到一个满足条件的元素
groupBy:按条件分组,结果是map
flatMap:先根据作为实参给定的函数对集合中的每个元素做映射,然后把多个列表合并平铺成一个列表。
flatten:多个列表合并平铺成一个列表
举个例子:
fun main() {
val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
val i = list.map { it * it }.filter { it > 20 }.find { it > 0 }
}
首先我们创建了一个list,使用map将每一个元素平方,然后筛选出大于20的元素,再从剩下的集合中找到一个大于0的元素。我们看下对应的java code:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import kotlin.Metadata;
import kotlin.collections.CollectionsKt;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
d2 = {"main", "", "LearnKotlin"}
)
public final class HelloKt {
public static final void main() {
List list = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6, 7});
Iterable $receiver$iv = (Iterable)list;
Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
Iterator var5 = $receiver$iv.iterator();
Object element$iv$iv;
int it;
boolean var8;
while(var5.hasNext()) {
element$iv$iv = var5.next();
it = ((Number)element$iv$iv).intValue();
var8 = false;
Integer var12 = it * it;
destination$iv$iv.add(var12);
}
$receiver$iv = (Iterable)((List)destination$iv$iv);
destination$iv$iv = (Collection)(new ArrayList());
var5 = $receiver$iv.iterator();
while(var5.hasNext()) {
element$iv$iv = var5.next();
it = ((Number)element$iv$iv).intValue();
var8 = false;
if (it > 20) {
destination$iv$iv.add(element$iv$iv);
}
}
$receiver$iv = (Iterable)((List)destination$iv$iv);
Iterator var13 = $receiver$iv.iterator();
Object var10000;
while(true) {
if (var13.hasNext()) {
Object var14 = var13.next();
int it = ((Number)var14).intValue();
int var16 = false;
if (it <= 0) {
continue;
}
var10000 = var14;
break;
}
var10000 = null;
break;
}
Integer i = (Integer)var10000;
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
通过java code我们可以看到在集合的操作过程中产生了一个临时集合destination$iv$iv
,这样对性能是有一定影响的。其实这些函数式API每进行一次操作,就会将所有元素赋值到临时数组中,实际上我们有更好的方式来减少性能损耗。就是使用asSequence
将集合转化为序列。
asSequence
将集合转换成序列之后,操作符分成了两类,一种是中间操作符,一种是末端操作符。中间操作符例如map、filter,末端操作符如asList,中间操作符的实现是懒加载,也就是说在遇到末端操作符之前是不会实际运行的。
还有一点,就是计算执行顺序不同。正常集合的操作是每进行一次函数式操作,都会产生一个中间集合。而序列的的顺序是,针对每一个元素都进行完整的所有操作。这样的好处在于在某些情况下可以提升程序执行效率。
举例来说:
对于集合来说:
val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
val i = list.map { it * it }.find { it > 20 }
它的执行顺序如图:
而对于序列来说:
val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
val i = list.asSequence().map { it * it }.find { it > 20 }
它的执行顺序如图:
很明显,序列的操作省去了元素6和7的map操作。执行效率更高。
集合的map等函数都是inline的,所以它们执行少量数据时效率很高,因为它们不会把lambda转换为对应的类。而序列的map等函数是非inline的,它们在执行大量数据时效率较高,因为不用产生临时对象。
with/apply/let/also/run
我们看下这几个函数的定义:
/**
* Calls the specified function [block] and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
/**
* Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public c fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
/**
* Calls the specified function [block] with `this` value as its argument and returns `this` value.
*/
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
可以看到这几个函数都是inline
的。
分别看一下每个方法是如何使用的:
run函数(直接调用)
/**
* Calls the specified function [block] and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
run函数里直接传递一个lambda表达式,lambda的返回值就是run函数的返回值。
run函数(通过类调用)
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
通过T类来调用run函数,lambda表达式可以通过this
引用类中的方法或属性,this
可以省略。lambda的返回值就是run函数的返回值。
with函数
/**
* Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
使用T作为接受者,lambda表达式中可以使用this
引用类中的方法或属性,this
可以省略。with的返回值是lambda的最后一行。
apply函数
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public c fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
通过T类调用run方法,lambda表达式中可以使用this
引用类中的方法或属性,this
可以省略。apply的返回值是T。
also函数
/**
* Calls the specified function [block] with `this` value as its argument and returns `this` value.
*/
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
also方法从1.1版本开始,通过T类调用also方法,lambda表达式中需要传递T类作为参数,lambda表达式中使用it
引用类中方法或者属性。also方法返回值是T。
let函数
/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
let方法通过T类调用,lambda表达式中需要传递T类作为参数,lambda表达式中使用it
引用类中方法或者属性。also方法返回值是lambda的返回值。