Kotlin 是一种现代但已经成熟的编程语言,旨在让开发人员更快乐。它简洁、安全、可与 Java 和其他语言互操作。 ------ Kotlin官网
Kotlin
是一门年轻的语言,由世界上IDE做的最好的Jetbrains 公司在2010年面向公众推出,直到2017年Google在I/O大会上宣布推荐Kotlin作为Android开发语言才进入Android开发者视野 ,2019年的I/O大会上,Google再度加码,宣布Kotlin为Android开发的首选语言,并且Android官方的类库代码将逐渐切换为Kotlin实现,至此确立Kotlin语言在Android开发中的地位。
正如官方文档说的那样,Kotlin
语法带有许多方便高效的语法设计,但又与 Java
完美兼容。有人说这些只不过是Kotlin的 「语法糖」 ,本质还是JVM
那一套,本篇文章旨在剥开这层糖衣,探究 Kotlin 的一些语法特性在编译后的本质。
函数的作用域
顶级函数 top-level functions
在Java中,任何函数与变量都需要声明在一个类中,Kotlin摒弃了这个设定,允许开发者脱离类和接口定义函数与变量,Kotlin称之为 顶级函数
。 我们尝试在Android Studio
中声明一个顶级函数来探究本质:
kotlin
package com.example
fun topFunction(): String {
return "这是一个顶级方法"
}
借助 IDE 提供的一些能力,我们可以很方便的看到这段Kotlin代码等价的Java代码,具体操作路径如下: 上面这段代码对应的Java代码如下:
java
package com.example;
public final class TopLevelDemoKt {
@NotNull
public static final String topFunction() {
return "这是一个顶级方法";
}
}
可以看到,所谓的脱离类限制的顶级函数,实际上在Java中其实还是处于一个类中,这个类的名字由Kotlin文件名 + Kt组成,对应的方法变成了Java中的静态方法。于是我们在Java代码中可以这样调用上面那段Kotlin的顶级函数:
java
public class UsingTopLevelFunctionInJava {
public static void main(String[] args) {
String text = TopLevelDemoKt.topFunction();
}
}
我们还也可以通过注解@file:JvmName
指定生成的类名:
kotlin
@file:JvmName("Top")
package com.example
fun topFunction(): String {
return "这是一个顶级方法"
}
对应的Java代码调用也就变成了Top.topFunction()
。
本地函数 local functions
下面展示一段由Kotlin编写的模拟校验用户注册提供的账号密码是否合规的代码,我们假设用户账户或者密码长度小于7为不合规,直接抛出异常:
kotlin
fun registrationCheck(username: String, password: String): Boolean {
var variableOutsideTheLocalFunction = 1
fun validateInput(input: String){
// 本地方法可以引用到方法外的变量
variableOutsideTheLocalFunction++
if (input.length < 7) {
throw IllegalArgumentException("The length must be greater than 7")
}
}
validateInput(username)
validateInput(password)
return true
}
和我们认知中的Java代码不同,该代码的validateInput
函数写在了registrationCheck
方法体里,这种"函数的函数"的形式,Kotlin官方称之为本地函数Local Functions
。 可以看到的是,本地函数可以访问外部函数的局部变量,在上面的例子中,variableOutsideTheLocalFunction
定义在本地方法之外,但仍旧可以在本地方法内引用到。 这又是什么magic呢?我们来看与上面代码等价的Java代码:
java
public final class KotlinDemoKt {
public static final boolean registrationCheck(@NotNull String username, @NotNull String password) {
Intrinsics.checkNotNullParameter(username, "username");
Intrinsics.checkNotNullParameter(password, "password");
final Ref.IntRef variableOutsideTheLocalFunction = new Ref.IntRef();
variableOutsideTheLocalFunction.element = 1;
<undefinedtype> $fun$validateInput$1 = new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke((String)var1);
return Unit.INSTANCE;
}
public final void invoke(@NotNull String input) {
Intrinsics.checkNotNullParameter(input, "input");
int var10001 = variableOutsideTheLocalFunction.element++;
if (input.length() < 7) {
throw (Throwable)(new IllegalArgumentException("The length must be greater than 7"));
}
}
};
$fun$validateInput$1.invoke(username);
$fun$validateInput$1.invoke(password);
return true;
}
}
代码相对来说还是比较好理解的,关键就是我们在Kotlin中定义的本地函数validateInput
在Java中被声明成了一个Function1
的实现,并通过方法invoke
进行调用,我们一起来看看这个Funtion1究竟是何方神圣:
java
/** 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
}
/** 省略 N 个 相似代码... */
/** 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
}
进入kotlin.jvm.functions.Functions.kt
文件下面,我们能看到这里预定义了很多类似的接口,实际上,Lambda 表达式就是实现了这些接口之一的类。
Kotlin 通过本地函数为开发者提供了一种比private更小的、更容易理解和维护的方法块,通过这种方式大大提高代码可读性和可复用性。
同时,我们应该注意到由于其特有的实现方式(匿名内部类),相比于直接使用私有函数,使用本地函数存在额外的性能开销,虽然这点开销微乎其微,但仍然需要开发者进行一定的取舍。
拓展
Kotlin支持为已经声明好的类扩展新的方法和新的属性。
kotlin
/**
* androidx.core.view.View.Kt中为View的扩展的visible属性
*/
public inline var View.isVisible: Boolean
get() = visibility == View.VISIBLE
set(value) {
visibility = if (value) View.VISIBLE else View.GONE
}
/**
* androidx.core.view.View.Kt中为View的扩展的用于更新padding的方法
*/
public inline fun View.setPadding(@Px size: Int) {
setPadding(size, size, size, size)
}
等价的Java代码大概是这样的:
java
public static final boolean isVisible(@NotNull View $this$isVisible) {
int $i$f$isVisible = 0;
Intrinsics.checkNotNullParameter($this$isVisible, "$this$isVisible");
return $this$isVisible.getVisibility() == 0;
}
public static final void setVisible(@NotNull View $this$isVisible, boolean value) {
int $i$f$setVisible = 0;
Intrinsics.checkNotNullParameter($this$isVisible, "$this$isVisible");
$this$isVisible.setVisibility(value ? 0 : 8);
}
public static final void setPadding(@NotNull View $this$setPadding, @Px int size) {
int $i$f$setPadding = 0;
Intrinsics.checkNotNullParameter($this$setPadding, "$this$setPadding");
$this$setPadding.setPadding(size, size, size, size);
}
可以看到,在等价的Java代码中,拓展函数的第一个参数是被拓展类本身,第二个参数才是拓展函数中定义的参数,同样,拓展属性也是通过拓展对应的getter
/ setter
方法来实现的。
拓展的出现,可以让我们无需修改类的源代码就能向现有的类中添加新的功能,降低了修改代码带来的风险,同时也使得代码的可读性更好。Android官方也在
androidx
加入了许多好用的拓展属性和拓展方法。
默认参数
使用Java代码编写Android中的自定义View大概是这样式儿的:
java
public class MViewWithJava extends View {
public MViewWithJava(Context context) {
this(context, null);
}
public MViewWithJava(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MViewWithJava(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MViewWithJava(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// 省略相关业务逻辑...
}
}
一行业务相关代码没写,光是构造函数声明了4个,最终都调用到了4个参数的构造函数中。这段代码如果使用Kotlin写呢?
java
class MViewWithKotlin @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
) : View(context, attrs, defStyleAttr, defStyleRes) {
init {
// 省略相关业务逻辑代码
}
}
可以看到得益于默认参数,代码明显简洁清晰了许多,这又是什么magic呢?我们来实验一下:
kotlin
fun testDefaultArgs(p1: Int, p2: String = "test"){
print("$p1$p2")
}
其对应的等价Java代码如下:
kotlin
public static final void testDefaultArgs(int p1, @NotNull String p2) {
Intrinsics.checkNotNullParameter(p2, "p2");
String var2 = p1 + p2;
System.out.print(var2);
}
// $FF: synthetic method
public static void testDefaultArgs$default(int var0, String var1, int var2, Object var3) {
if ((var2 & 2) != 0) {
var1 = "test";
}
testDefaultArgs(var0, var1);
}
可以看到,除了正常的testDefaultArgs方法外,等价的Java代码中还多出了一个testDefaultArgs$default
方法,看方法名也知道这是实现默认参数的关键,该方法中除了我们声明的一个int参数和一个String参数外,还多出了一个int类型的参数和一个Object类型的参数。这个方法中的代码也很好理解,var2作为标记位,由编译器根据实际的代码进行赋值传参,var2 & 2
中的2表示是第几位参数有默认参数,与var2做一些逻辑逻辑与操作判断对应的参数是否传递,若未传递就进行默认值赋值操作。
总结
Kotlin中语法的特性除了上述总结的顶级函数、本地函数、拓展属性和方法以及默认参数外,还有很多,比如解构函数、智能转换以及空安全等等等等,受限于篇幅原因不可能一一探索其背后的实现。本文旨在抛砖引玉,如果你也对Kotlin的某个语法糖感到好奇,不妨亲自揭开他的糖衣,探究其背后的故事~