前言
用了kotlin 很多年了,kotlin使用的方便性大大提高了我们的开发效率。android结合kotlin的特性,推出的compose、coroutine、Flow、Channel、Room等框架大大提升了我们的开发效率。人无法从有知状态变为无知状态,如果有很多跟我一样,是先学的java,再学的kotlin,是不是会感叹kotlin语法糖的强大。对于kotlin的用法已经有很多的教程,今天我通过将kotlin转换成Java代码的方式,从java的角度去看待kotlin到底做了什么,让我们开发如此的简便。
那么废话不多说,我们直接开始。
这里我们找了很多大家比较常用的一些语法糖。(本篇主要是把自己理解kotlin语法的方式分享给大家,不卖鱼、只分享捕鱼的方式)
顶层函数
Java一切皆对象,不过kotlin的函数可以不放在class内部,直接写成顶层函数调用,我们都知道,kotlin最终也会转换成字节码运行在JVM上的,那么顶层函数到底是什么形式?
例如:
大家可以看到,我们定义了一个顶层函数,他不属于任何一个class里面。那么他的本质是什么呢? 我们通过android studio转换出字节码来看一下:
swift
// class version 52.0 (52)
// access flags 0x31
public final class com/yuanyi/myapplication/kt/AlwaysLearnKt { // 可以看到,我们在外部定义的AlwaysLearn.kt文件被创建成为了一个final类
// access flags 0x19
// 我们的topFunction被定义成了一个final static类型的静态函数,
// ()V指代该方法没有任何参数,V代表返回值为Void
public final static topFunction()V
L0
LINENUMBER 4 L0
RETURN
L1
MAXSTACK = 0
MAXLOCALS = 0
// 这里指代当前是一个kotlin的文件,对应的kotlin的版本号是1.8.0
@Lkotlin/Metadata;(mv={1, 8, 0}, k=2, d1={"\u0000\u0008\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001\u00a8\u0006\u0002"}, d2={"topFunction", "", "app_debug"})
// compiled from: AlwaysLearn.kt
}
然后我们把字节码反编译成java代码看一下:
这个就很简单了是不是,我们发现,所谓的顶层函数,本质上就是一种静态方法。
扩展函数
顶层类扩展函数
我们查看一下字节码
swift
// class version 52.0 (52)
// access flags 0x31
public final class com/yuanyi/myapplication/kt/ExtendFunctionKt {
// access flags 0x19
// 可以看到,我们定义的顶层扩展函数被转换成了final static的方法
// (Ljava/lang/String;)I 代表参数是string
// 返回值一个不为空的int因为我在定义返回值的时候,用的是Int不是Int?
public final static getLength(Ljava/lang/String;)I
// annotable parameter count: 1 (visible)
// annotable parameter count: 1 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "$this$getLength"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 4 L1
ALOAD 0
INVOKEVIRTUAL java/lang/String.length ()I
IRETURN
L2
LOCALVARIABLE $this$getLength Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
@Lkotlin/Metadata;(mv={1, 8, 0}, k=2, d1={"\u0000\u000c\n\u0000\n\u0002\u0010\u0008\n\u0002\u0010\u000e\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002\u00a8\u0006\u0003"}, d2={"getLength", "", "", "app_debug"})
// compiled from: ExtendFunction.kt
}
然后我们将字节码反编译为java看一下
我们发现,这个方法的确是一个public static final int getLength(String)的形式。
为了效率,我们后面不再看字节码了,最后我会把查看字节码和转Java代码的方式写出来。
类内部扩展函数
kotlin
package com.yuanyi.myapplication.kt
// 这是一个顶层的String类型的扩展函数,获取string的长度
fun String.getLength() : Int = this.length
class ExtendFunction{
// 这是一个类内部的扩展函数
fun String.println() = println(this)
}
对应的Java代码
kotlin
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
@Metadata(
mv = {1, 8, 0},
k = 1,
d1 = {"\u0000\u0016\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\u0010\u000e\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\n\u0010\u0003\u001a\u00020\u0004*\u00020\u0005¨\u0006\u0006"},
d2 = {"Lcom/yuanyi/myapplication/kt/ExtendFunction;", "", "()V", "println", "", "", "app_debug"}
)
// 这是我们在kt里面定义的class
public final class ExtendFunction {
// 我们发现,我们定义的类的内部扩展函数,是类的一个final类型的成员函数
// 它接收一个String类型的参数,然后打印它
public final void println(@NotNull String $this$println) {
Intrinsics.checkNotNullParameter($this$println, "$this$println");
System.out.println($this$println);
}
}
// ExtendFunctionKt.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
@Metadata(
mv = {1, 8, 0},
k = 2,
d1 = {"\u0000\f\n\u0000\n\u0002\u0010\b\n\u0002\u0010\u000e\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002¨\u0006\u0003"},
d2 = {"getLength", "", "", "app_debug"}
)
// 这是我们的文件名,在编译的时候,kotlin都会为我们生成一个"${文件名}kt"的类
// 我们定义的顶层扩展函数,是这个类的静态函数
public final class ExtendFunctionKt {
public static final int getLength(@NotNull String $this$getLength) {
Intrinsics.checkNotNullParameter($this$getLength, "$this$getLength");
return $this$getLength.length();
}
}
相信看到这里,大家大致对kotlin写类外的代码有了一定的了解了,我们定义的顶层函数,本质上是被写入到文件名生成的类内部的,这也符合了java一切切对象的设计。
block到底是什么?
rust
block1 : (String?,String?) -> String?
block2 : (String?) -> Int
block3 : (String?) -> Unit
相信大家都用过上面的代码吧,其实我告诉大家,它本质就是一个接口,是一个只有一个方法的接口。我们来看看它对应的代码是什么。
首先,定义一个用block的方法
kotlin
class BlockTest {
// 定义一个获取string长度的方法
fun getStringLength(
string : String,
block : ((String) -> Int)
) = block(string)
}
对应的Java的代码为:
less
@Metadata(
mv = {1, 8, 0},
k = 1,
d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J"\u0010\u0003\u001a\u00020\u00042\u0006\u0010\u0005\u001a\u00020\u00062\u0012\u0010\u0007\u001a\u000e\u0012\u0004\u0012\u00020\u0006\u0012\u0004\u0012\u00020\u00040\b¨\u0006\t"},
d2 = {"Lcom/yuanyi/myapplication/kt/BlockTest;", "", "()V", "getStringLength", "", "string", "", "block", "Lkotlin/Function1;", "app_debug"}
)
public final class BlockTest {
// 我们发现,
// 我们定义的方法当中的block变成了一个Function1的类型的对象,
// 他通过调用invoke方法来调用我们定义的方法,
// 然后讲结果强转为Number,
// 然后通过intValue()返回
public final int getStringLength(@NotNull String string, @NotNull Function1 block) {
Intrinsics.checkNotNullParameter(string, "string");
Intrinsics.checkNotNullParameter(block, "block");
return ((Number)block.invoke(string)).intValue();
}
}
查看这个Function1是什么?
我们发现,这个Function1是属于kotlin.jvm.functions里面定义的一种interface,他接收一个P1类型的数据,然后返回一个R类型的数据,发现了没有,这个形式跟我们定义的block是不是很相似。
kotlin
// block的形式
(String) -> Int
// 可以看成
fun method(string : String) : Int
// Function1的形式
fun invoke(p1:P1) : R
所以,本质上,我们的block就是一个被定义过的interface,只不过kotlin的编译器帮我们做了转换。
其中block最多接受参数个数为22个,超过22个就会报错。但是我们通常不会有超过22个的参数,超过了,那么就封装一下。
let、with、apply、run、alse、takeIf...到底是什么?
为此,我写了如下的例子:
kotlin
class ExtendFunction{
fun test_Let_Run_Also_Apply_With_TakeIf (){
val data = "hello"
// let
val letResult = data.let {
Log.i(data, "let block")
it.length
}
// apply
val applyResult = data.apply {
Log.i(data, "apply block")
length
}
Log.i(data, applyResult)
// also
val alsoResult = data.also {
Log.i(data, "also block")
it.length
}
Log.i(data, alsoResult)
// run
val runResult = data.run {
Log.i(data, "run block")
length
}
Log.i(data, runResult.toString())
// with
val withResult = with(data){
Log.i(data, "with block")
length
}
Log.i(data, withResult.toString())
// takeIf
val takeIfResult = data.takeIf {
Log.i(data, "takeIf block")
it == "hello"
}
Log.i(data, takeIfResult.toString())
}
}
将上述代码转换为java代码之后,我们查看一下
我们发现,block在这里的方式不直观(这是因为let这些扩展函数被inline标记了,)直接被隐藏了,我们先说一下部分结果
- let是执行完block里面的代码,直接将block最后一行的结果返回给我们
- apply是执行完block里面的代码,直接将string返回给我们
- 其余的略 我们发现,好像也没什么大区别嘛。
我们查看一下apply、let他们的定义
apply
kotlin
@kotlin.internal.InlineOnly
// 方法定义,它被定义是一个范型T类型的一个扩展函数,
// 它接受一个block为参数
// 然后返回这个T对象(从这里,大家就知道为什么apply会返回对象本身了)
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
其实转换成java代码就是(由于java代码没有内联,因此这里模拟一下)
r
public <T> T apply(T t, Function1<T> function1){
...
function1.invoke(t);
return t;
}
let
kotlin
@kotlin.internal.InlineOnly
// let也是一个范型T的一个扩展函数,
// 它接收一个block:(T) -> R类型的对象,
// 然后将R类型返回
// 所以各位知道了吧,为什么block()的最后一行就是let的返回值
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
转换成java代码就是
其实转换成java代码就是(由于java代码没有内联,因此这里模拟一下)
r
public <T,R> T let(T t, Function1<T> function1){
...
R r = function1.invoke(t);
return r;
}
后续的就不再举例子了。 因此,如果有面试官问你,apply、with、let有什么区别,什么场景下使用它。我认为,其实他们这些很大程度上都是可以通用的,就看你喜欢用什么。
object class到底是什么?
我们定义一个object看下先
kotlin
object INSTANCE{
fun sayHello() = println("Hello , instance!")
}
然后转换成java代码查看
java
public final class INSTANCE {
@NotNull
public static final INSTANCE INSTANCE;// INSTANCE实例对象
public final void sayHello() {
String var1 = "Hello , instance!";
System.out.println(var1);
}
// 将构造函数设置为私有
private INSTANCE() {
}
// 通过静态代码块在类加载的时候就直接对类进行初始化
static {
INSTANCE var0 = new INSTANCE();
INSTANCE = var0;
}
}
我们发现,其实它本质上,就是一个饿汉的单例模式。
什么是伴生对象
为什么它也能当作是单例模式? 我们先写一个伴生对象的类
kotlin
class INSTANCE{
companion object{
val data : String = "hello"
}
}
然后转化成java代码查看
java
public final class INSTANCE {
@NotNull
private static final String data = "hello";
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
// 我们发现,其实compaion也是一个静态内部类,通过Compaion去访问
public static final class Companion {
@NotNull
public final String getData() {
return INSTANCE.data;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
发现Companion是一个静态内部类,外部的INSTCANCE持有了这个内部类。
委托模式(by)
我们看到委托模式,是不是有点像代理模式?待会我们再说一下代理模式和委托模式的区别,先来看一个例子。
kotlin
// 先定义一个接口
interface A{
fun say(words : String)
}
// 定义一个实现类
// 构造函数接收一个A,
// 然后将Speaker对应A的功能全权交给a
class Speaker(a : A) : A by a
那么关键by到底做了什么呢?答案我们就可以通过java代码获取
less
// Speaker的实现类
public final class Speaker implements A {
// $FF: synthetic field
private final A $$delegate_0;// 内部有一个delegate的A对象
// Speaker的构造函数,接收一个A对象a
public Speaker(@NotNull A a) {
Intrinsics.checkNotNullParameter(a, "a");
super();
this.$$delegate_0 = a;
}
// Speaker的say方法,可以看到,直接调用了delegate的方法
public void say(@NotNull String words) {
Intrinsics.checkNotNullParameter(words, "words");
this.$$delegate_0.say(words);
}
}
// A.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
import org.jetbrains.annotations.NotNull;
// 定义了一个接口
public interface A {
void say(@NotNull String var1);
}
所以,通过java代码看,其实很简单,很像我们的代理模式,只不过我们使用代理模式的目的是为了增强被代理对象的能力。而kotlin的by关键字就是全权交给被委托的对象来操作。
以上举的个别例子只是kotlin众多语法糖的冰上一角。当我们去使用kotlin的语法的时候,要做到知其然,知其所以然。那么通过kotlin->Java代码的方式,对于有java基础的同学去理解kotlin本身是非常好的一种方式。后面我们就来介绍一下kotlin->Java的方法,很简单的。
kotlin->Java
首先,在android studio上,安装一个名为"Kotlin to Java Decompiler"插件,通过Settings->Plugin->Market找到这款插件,如下图:
安装之后重启一下AS。
随便写一个kotlin的代码,然后通过tools->kotlin->Show Kotlin Bytecode
就可以得到kotlin对应的class字节码
点击Decompile就可以得到java代码
。
本次分享比较简单,重点是分享一下自己是如何理解kotlin语法糖背后的原理的一些方式的。希望能帮到大家。在理解过程中有疑问的,可以留言,大家一起讨论一下。