内容简介
上一篇我了解到了 `Kotlin` 很重要的一个角色 `Lambda` 表达式,并且了解到了它的本质。接下来我们来看下 `Kotlin` 为我们特有类型的函数,方便我们开发。
Kotlin
创建文件都是创建 .kt
文件,而 Java
创建的是 .java
文件。在 Java
文件中只能定义一个类(当且不考虑内部类),并且名字要和文件名相同,文件路径对应的就是包名,这些都是乌龟的腚(规定)。
Kotlin
随和多了,定义的 kt
文件不再是类的约束。 kt
文件只是一个编写代码的容器,我们可以随意定义多个类,你要是喜欢可以把整个程序都编写在一个 kt
文件中,并且包名也不再是必须是文件路径了。最重要的是我们可以直接在 kt
文件定义方法和变量,我们称为 包级函数
丶 包级变量
。
我先定义了一个 Kot.kt
文件,对应包名 com.qihoo.Kot
(也就是路径名)
接下来我修改了包名 com.qihoo.test
编译器没报错,并且直接定义了 main
丶 call
方法,以及 2
个变量。
/**
* 修改包名
*/
package com.qihoo.test
/**
* 直接定义 name 变量
*/
val name = "阿文"
val age = 18
/**
* 在定义一个方法
*/
fun call(name: String, age: Int) {
println("$name:$name")
}
/**
* 直接定义类
*/
data classProgrammer(val name: String, val age: Int)
/**
* main方法直接卸载 kt 文件了
*/
fun main() {
Programmer(name, age)
call(name, age)
}
我们来看下产物,看看生成的 class
是什么?生成了一个 KotKt.class
文件,并且路径是 com/qihoo/test
,是我们修改包名路径和 kt
文件路径无关。
这里我遇到过一个坑,做
A
项目的时候,拷贝了自己B
项目的kt
文件,忘记修改包名了。在混淆的时候配置的混淆配置不对,导致出了一些问题。所以在写Kotlin
的时候,一定要记住kt
文件的路径,并不是生成类的路径。
我们来看看生成的 class
文件的源码,看看到底是产物是什么(其实大家猜也能猜到)?包级函数&变量存在于对应 文件名.class
类中的静态变量&方法
publicfinalclassKotKt{
privatestaticfinalint age = age;
@NotNull
privatestaticfinalString name = name;
@NotNull
publicstaticfinalString getName() {
return name;
}
publicstaticfinalint getAge() {
return age;
}
publicstaticfinalvoid call(@NotNullString name2, int age2) {
Intrinsics.checkParameterIsNotNull(name2, "name");
System.out.println(name2 + ':'+ name2);
}
publicstaticfinalvoid main() {
newProgrammer(name, age);
call(name, age);
}
}
看到了生成的类,我们应该知道了 java
如何调用这些方法和变量了。通过 KotKt
调用静态方法即可。
publicclassDemo{
publicstaticvoid main(String[] args) {
String name = KotKt.getName();
int age = KotKt.getAge();
KotKt.call(name, age);
}
}
我们 Java
调用这些包级函数,还有编写 文件名Kt
来进行调用,很不直观, Kotlin
为我们提供了 @file:JvmName("生成的类名")
注解来修改生成的类名(注意混淆哦),直接在 kt
文件中编写即可。
后续会有专门的一篇,讲解
Kotlin
为我们提供的几个注解,来方便我们与Java
之间的互相调用。
相比较 Kotlin
调用就比较直观了,直接导包调用即可。我们在创建一个 kt
文件,尝试调用。是不是超级简单呀?
/**
* 导入用的变量丶方法的包
*/
import com.qihoo.test.age
import com.qihoo.test.call
import com.qihoo.test.name
fun main() {
/**
* 直接调用
*/
call(name, age)
}
这补充一个知识点,前面应该也说过。我们知道包级函数&变量的调用都是通过导包的,这样就会有一个奇异,如果
2
个不同包的2
个kt
文件中,定义了2
个同样的包级函数怎么办呢?
例如:
com.qihoo.kot.Kot.kt
文件
package com.qihoo.kot
/**
* 定义 call 方法
*/
fun call() {
println("我是在 com.qihoo.kot.kot.kt 文件定义的哦")
}
com.qihoo.kot2.Kot.kt
文件
package com.qihoo.kot2
/**
* 定义 call 方法
*/
fun call() {
println("我是在 com.qihoo.kot2.kot.kt 文件定义的哦")
}
我们尝试的调用,会报错哦。因为编译器不知道要调用那个 call
方法。这时候我们可以写全路径的方式调用,也可以通过 as
关键词重新命名,来解决调用歧义。
package com.qihoo.core
/**
* 导入2个 call 方法,并通过 as 关键词,重新定义调用昵称
*
* 思考 as 关键词还在哪里用过呢?
*/
import com.qihoo.kot.call as kotCall
import com.qihoo.kot2.call as kot2Call
fun main() {
// 直接使用 重新定义调用即可
kotCall()
kot2Call()
}
不得不说 Kotlin
的扩展函数和 Lambda
简直将 Kotlin
推向了高潮。它给使用 Kotlin
的开发者们增加了无限的想象。
曾经我们写了无数的工具类,一般都是抽取静态方法。这种形式比较恶心,并且在协同开发过程,经常出现重复造轮子的工具类。而 Kotlin
的扩展方法,能优化一部分这样的问题(后续讲到的 Kotlin
的高阶函数,大部分都是通过扩展方法实现的)。
例如:我们经常数组有一个交换操作(以前是通过抽取工具类静态方法形式),接下来我们定义一个扩展函数来实现。需要对什么类进行扩展,只需要使用 类名.扩展函数昵称(参数1:类型,参数2:类型)
。
/**
* 定义扩展扩展方法swap,我们是定义在了Array扩展上
*/
fun Array.swap(v1: Int, v2: Int) {
val tmp = this[v1]
this[v2] = this[v1]
this[v1] = tmp
}
fun main() {
val arrays = arrayOf("1", "2", "3")
/**
* 可以发现 直接有 swap 方法了(编辑器会为我们提示此方法哦)
*/
arrays.swap(0, 1)
}
是不是很神奇?扩展函数的函数体,既然能直接调用扩展类的变量和方法,难道编译期把扩展方法插入到对应的扩展类里面了?仔细想想也不可能,如果我为 Activity
扩展方法,你怎么可能注入到 Activity
类中,这些类都是在 ROM
中的。那他到底怎么实现的呢?我们来看下编译结果。
其实也很简单,就是多生成了一个静态方法,对应的第一个参数就是我们扩展类的调用对象罢了。从本质我们就能看出一个问题,扩展函数的函数体只能调用访问扩展类的公开方法和属性。现在知道为啥 kotlin
定义的变量和函数都是默认公开的了吧。
publicfinalclassCallKt{
publicstaticfinal void swap(@NotNull T[] $receiver, int v1, int v2) {
Object tmp = $receiver[v1];
$receiver[v2] = $receiver[v1];
$receiver[v1] = tmp;
}
publicstaticfinalvoid main() {
swap(newString[]{"1", "2", "3"}, 0, 1);
}
}
Kotlin
为我们封装的方法几乎都是以扩展函数的形式提供。文件读写,数组增删改查丶遍历都是以扩展函数的形式增加,大家可以创建一个对象,在通过代码提示看看有多少扩展函数吧。
在讲解内联函数之前,大家可以了解下
Java
虚拟机的线程是如何调用方法的,线程执行完毕的依据又是什么呢(后面的扩展内容会讲到)?
我们如何定义内联函数呢?其实很简单只需要通过 inline
修饰方法即可,接下来我们来看看通过 inline
修饰后和未修饰后的区别吧。
/**
* 未经过inline修饰的fun方法
*/
fun call(block: () -> Unit) {
println("1")
block()
println("2")
}
/**
* 调用执行
*/
fun main() {
call {
println("3")
}
}
看下编译的 Java
信息,我们可以看到 main
方法单纯的就是调用了 call
方法。
publicfinalclassKotlinInlineKt{
publicstaticfinalvoid main() {
// 1. 可以看到创建了一个Function0的对象,并且调用了call方法
call((Function0)null.INSTANCE);
}
// $FF: synthetic method
publicstaticvoid main(String[] var0) {
main();
}
// 2.call方法的定义没啥好说的
publicstaticfinalvoid call(@NotNullFunction0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var1 = "1";
boolean var2 = false;
System.out.println(var1);
block.invoke();
var1 = "2";
var2 = false;
System.out.println(var1);
}
}
接下来我们尝试通过 inline
修饰方法的结果( Koltin
代码就不贴出来了,就是 call
方法多了个 inline
修饰符)。我们可以发现 main
方法没有直接调用 call
方法了,而是将 call
方法的函数复制了一份,直接写在 main
方法中了。试想下这样的坏处,就是单独的 class
文件的体积会变大(怪不得 Kotlin
的项目体积明显比 Java
的大)。
publicfinalclassKotlinInlineKt{
// 1. 看main方法都不调用call方法了和创建lambda对应的FunctionN对象了,直接将call方法的代码拷贝了一份(看上去打破了java的方法抽取服用的概念)
publicstaticfinalvoid main() {
int $i$f$call = false;
String var1 = "1";
boolean var2 = false;
System.out.println(var1);
int var3 = false;
String var4 = "3";
boolean var5 = false;
System.out.println(var4);
var1 = "2";
var2 = false;
System.out.println(var1);
}
// $FF: synthetic method
publicstaticvoid main(String[] var0) {
main();
}
// 2.call方法的定义没啥好说的
publicstaticfinalvoid call(@NotNullFunction0 block) {
int $i$f$call = 0;
Intrinsics.checkParameterIsNotNull(block, "block");
String var2 = "1";
boolean var3 = false;
System.out.println(var2);
block.invoke();
var2 = "2";
var3 = false;
System.out.println(var2);
}
}
这里有个点不知道大家有没有注意,通过
inline
修饰的方法,若行参中有Lambda
表达式,也会默认将Lambda
代码平铺到调用者函数中。试想下这样的好处是啥?在上一篇中我们学习过Lambda
表达式的本质,会多生成一个FunctionX
的类,如果这样做是不是就会少生成1
个类呀?当然若我不想让Lambda
也被内联也是可以的,只需要通过noinline
修饰Lambda
表达式。
/**
* 经过inline修饰的fun方法
* 通过 noinline 关键词修饰lambda表达式,表示这个表达式在 inline 函数中, lambda 不会被平铺代码.最终还是会以 Function 对象的形式调用
*/
inline fun call(noinline block: () -> Unit) {
println("1")
/**
* 此 lambda 并不会平铺到代码中去
*/
block()
println("3")
}
不知道大家有没有考虑,内联函数中编写 return
会中断调用函数吗?会不会将内联函数的 return
代码编译到调用者函数中呢? Kotlin
开发者可能是为了防止歧义,内联函数的 return
是不会中断调用函数的。但是默认内联函数的 Lambda
表达式的 return
会中断。
/**
* 经过inline修饰的fun方法
*/
inline fun call(block: () -> Unit) {
println("1")
block()
println("3")
}
/**
* 验证下结论
*/
fun main() {
/**
* 调用内联函数
*/
call {
println("测试结果")
return
}
}
输出结果:
1
测试结果
可以看到代码中没有输出 3
, call
方法就结束了。查看下编译源码,发现的确没有将输出 3
的语句写入。
publicfinalclassCallKt{
publicstaticfinalvoid call(@NotNullFunction0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
System.out.println("1");
block.invoke();
System.out.println("3");
}
publicstaticfinalvoid main() {
System.out.println("1");
System.out.println("测试结果");
// 没有将输出 3 的语句写入
}
}
接下来就有个问题,那如果我不想内联函数的 Lambda
中断函数呢?可通过 crossinline
修饰 Lambda
表达式。然后你会发现,若在 Lambda
表达式中写 return
会直接报错。
/**
* 经过inline修饰的fun方法
*/
inline fun call(crossinline block: () -> Unit) {
println("1")
block()
println("3")
}
/**
* 验证下结论
*/
fun main() {
/**
* 调用内联函数
*/
call {
println("测试结果")
// 这句代码会报错
return
}
}
其实这里有必要说下,
Lambda
表达式本身是不允许写return
的。只有在内联函数且没有通过crossinline
修饰的Lambda
才可以调用return
(可以直接中断调用函数)。那问题又来了,若我们的Lambda
要有根据条件return
的功能呢?只能通过ifelse
吗?其实我们只需要使用return@函数昵称
即可。
/**
* 经过inline修饰的fun方法
*/
fun call(block: () -> Unit) {
println("1")
block()
println("3")
}
/**
* 验证下结论
*/
fun main() {
/**
* 调用内联函数
*/
call {
if(Math.random() > 0.5) {
// 注意这里
return@call
}
println("测试结果")
}
}
大家可以看下编译结果,还是很有意思的哦(不得不说 Kotlin
的编译器挺智能)。 Kotlin
提供给的很多扩展函数都是内敛函数,主要目的就是减少 Lambda
表达式生成的多余类。
以下内容都是扩展内容,懒得看可无视。
为何要有内联方法呢?他给我们带来了什么好处呢?我们来看下 Java
虚拟机是如何调用方法的吧!
栈帧概念:
这里要特别解释下栈帧,每个线程都会分配一个栈。这个栈中存放的数据就是栈帧,每个栈帧就代表的一个方法。
举个例子:当一个线程调用 a
方法, a
方法调用了 b
方法,那么这个线程的栈中就存放了 2
个栈帧,先将 a
方法压入栈,然后将 b
方法压入栈, b
方法执行完成弹出 b
, a
方法执行完弹出 a
。当这个栈为空了,这个线程也就算是执行完成了。
看到这里我想大家知道为啥要有内联函数了吧?它可以带来几个好处的。
减少线程调用栈的栈入栈出、
减少 Lambda
生成不必要的多余 FunctionX
类
推荐阅读
--END--
识别二维码,关注我们