日常工作中,我们总是希望提高效率,比如快捷键、自动化等,其实开发语言本身在兼容的情况下,也是可以提高效率的。比如Java
的lambda表达式
。 这里介绍的Kotlin
,并不是为了介绍Kotlin
语言的入门或者使用方式,而是和Java
对比,说明Kotlin
提高效率的方面,下面会用一些小例子,说明一下日常开发中的小提升。
兼容性 版本 Kotlin
会被编译为Java
字节码,并且可以指定Java
版本,这样一来,对于需要运行指定版本Java
代码的环境来说,就非常友好了。
双向 在Kotlin
的代码中是可以直接调用Java
类的方法的,因此对于老项目改造是很友好的。 而Java
中也可以直接使用Kotlin
的类以及方法,虽然使用的时候看起来有点奇怪,但是还是可以直接兼容的,因此对于老项目的升级,是没有问题的。
类型处理 类型推导 在Kotlin
中,申明变量是这样的:
1 2 val sdf = SimpleDateFormat("" )var sdf2 = SimpleDateFormat("" )
可以看到,前面就一个val
或者var
,看起来像是JavaScript
的弱类型啊,其实并不是。当申明变量并且直接初始化为明确类型的值时,可以自动推导类型,不用指定类型。 换句话说,有下面的情况
1 2 3 4 5 6 7 8 9 var index = 0 var time = 0L var temp = null var temp: String? = null
你可能会说,这个好像也没多大区别吧,但是对于下面这两种场景呢?
1 2 3 4 5 SimpleDateFormat sdf = new SimpleDateFormat("" ); MainFragment.LoadCallback loadCallback = new MainFragment.LoadCallback() { ... }; OutputStream out = xxx.getOutputStream();
这时候就很不友好了,每次接受一个对象需要写一遍类型再写名字。如果同样的内容换成kotlin
呢?
1 2 3 4 5 val sdf = SimpleDateFormat("" );val loadCallback = object : MainFragment.LoadCallback() { ... };val out = xxx.getOutputStream();
这样是不是就友好很多? 尽管编译器的.var
快捷命令,可以帮助我们申明类型,但是它代码本身就在那里,还是会影响视觉。
val
及var
的区别请前往中文官网自行查看。 另外,Java
高版本也支持了类型推导,但是个人认为没有Kotlin
做得好,而且有版本兼容问题。
非空判断 Java
中写的最多的,可能就是判空吧。
1 2 3 4 5 6 7 8 9 if (a != null ) { a.b(); } String b = "" ; if (c != null ) { b = c.d(); }
现在好消息是,Kotlin
里面可以大幅度的省略这部分代码了! 这分为两部分:类型非空,空判定
类型非空 上一个小节里面,指定变量类型的时候,在类型名称后面加上了一个?
,就像这样:
1 var temp: String? = null
这就是类型非空了,当一个变量申明时,如果变量类型后面携带了?
,或者类型推导时发现内容可能为null
,那么就会在调用时直接报错或者发出警告。提示你对象可能为null
,怎么调用这些可能为null的对象,是下一个标题的内容。 相反的,如果申明时没有携带?
,那么它就认为这个变量是不能赋值为null
的,如果你在后续的赋值中,赋值了null
,就会自己报错,连编译都过不了,就算侥幸骗过
了编译,也会在运行时赋值为null
的时候直接报错。 这样可以一定程度上避免代码单元内的空指针问题,也可以一定程度尽早发现空指针问题,同样,也少了很多的非空判断
空判定 上面说的是类型指定,那么使用呢?如果一个可能为null
的对象怎么使用?加if
?不用,可以直接这样写:(沿用小节开头的Java
例子)
1 2 3 4 5 6 7 8 9 a?.b(); val b = c?.d()?:"" ;fun test (a: A ?) { a?:return ... }
上面列举了三种常见的场景。 对于对象的方法调用,直接加上一个?
即可,等价于上面的if
中调用。 对于参数的赋值,可以在后面添加默认值,使用的是?:
。 对于方法内的参数判断,可以直接使用像是赋值一样的写法,然后执行return
操作。 代码是不是就简单了很多?
Getter & Setter Kotlin
帮我们做了很多的默认实现,这让我们的代码写起来更加轻松。 按照Java规范,我们写一个数据类一般是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Bean { private String a; private String b; private int c; public Bean (String a, String b, String c) { this .a = a; this .b = b; this .c = c; } public Bean () { this ("" , "" , 0 ); } public String getA () { return a; } public String getB () { return b; } public int getC () { return c; } public void setA (String a) { this .a = a; } public void setC (int c) { this .c = c; } }
可以看到,尽管只有三个成员变量,但是一个类还是非常的长,尽管有编译器的一键生成,但是代码仍然很多,不是很友好,而且全都是非常机械非常模板化的代码。那么在Kotlin
中会是什么效果,简化会达到什么程度呢?
1 data class Bean (var a: String = "" , val b: String = "" , var c: Int = 0 )
是不是感觉有点离谱?这里可以告诉你的是:确实已经写完了一个Bean
。那么它符合Java
规范吗?它是符合的,只是简化了这个过程,class的前面加上了data
,表示这是一个数据类,Kotlin
会自动为我们根据成员变量生成toString
和equals
等方法,我们也可以选择重写它,如果不需要特别重写,那么我们连类的大括号都不用写。 上面的Java
代码中,b
是不能被赋值的,这里只要把var
改成val
就好了。而getter
和setter
呢?Kotlin
中的每个变量都默认有个getter
和setter
的实现,也就是都帮我们写好了默认实现。 而成员变量的申明,因为变量会在构造器赋值,所以只要在构造器的参数中加上变量前缀,就自动变成了成员变量,当然,还可以前缀一个private
,让这个变量只能内部可见。 那么又有另一个问题了,三个参数都有默认值,我只想设定其中两个怎么办?Java
上的做法就只有提供足够多的重载了,但是Kotlin
里面可以这样写:
1 2 3 fun test () { val bean = Bean(a = "hello" , c = 2 ) }
是的,还可以直接使用变量名指定传值,这样连重载也不用了。 但是这里要说明一点,如果是需要反射调用的方法或者构造器,是需要单独申明的,否则是找不到的,毕竟参数数量本身没有变。
另一个方面,上面说了,Kotlin
帮我们写好了getter
和setter
,那么我们能重写吗?请看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class A { private val colorArray = intArrayOf(Color.RED, Color.GREEN, Color.BLUE) var index = 0 private set var color: Int get () { return colorArray[index] } set (value) { colorArray[index] = value } fun next () { index++ index %= colorArray.size } } fun test (a: A ) { val index = a.index val color = a.color a.color = Color.PINK }
可以看到,我们的A
中维护了一个颜色数组,提供了一个方法来移动下标。 外部可以拿到下标,也可以通过一个叫color
的变量来获取和设置当前下标对应的颜色。但是我们其实没有color
这个成员变量,我们只是通过完全重写getter
和setter
来模拟了一个变量。
扩展方法 都说Kotlin
是语法糖,但是这糖也是真的甜。比如这里介绍的扩展方法
。 它其实不是真的为对象类型增加了方法,只是从语法上做到了效果而已,但是使用的时候却可以大大提升效率。 举个Java
的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private String formatInt (int i) { if (i < 10 ) { return "0" + i; } return "" + i; } private void funA () { String str = formatInt( object1.funB( object2.funC()) .funD()); }
这是一个很简单的例子,但是看起来就不是很友好了,业务场景可能还会有更麻烦的结构。你可能会说多申明几个变量,然后分开写,但是那样又会显得很臃肿,写起来没有这种链条一样调用的爽快感觉。 所以可以这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 private fun Int.format(): String { if (i < 10 ) { return "0$i" } return "$i" } private fun funA () { val str = object1.funB(object2.funC()) .funD() .format() }
这个扩展方法就像它原本就有的方法一样,可以直接调用,完美的混入了链条
中。 而使用是,方法的增加和去掉,也不用像以前一样,考虑左边括号到哪里,右边括号是哪一个了。
函数式编程 Java
中是先有Class
再有方法,也就是必须要有对象。而在Kotlin
中,也是面向对象,但是它可以有顶级函数,也就是不依托于Class
的函数,当然,这也是语法糖而已。但是使用效果非常不错,我们先举例子再说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class TaskUtil { private static final Executor threadPool = Executors.newCachedThreadPool(); private static final Handler mainHandler = new Handler(Looper.getMainLooper()); public static void doAsync (ErrorCallback err, RunCallback run) { threadPool.execute(new Task(err, run)); } public static void onUI (ErrorCallback err, RunCallback run) { mainHandler.post(new Task(err, run)); } private static class Task extends Runnable { private ErrorCallback errorCallback; private RunCallback runCallback; public Task (ErrorCallback e, RunCallback r) { errorCallback = e; runCallback = r; } @Override public void run () { try { runCallback.run(); } catch (e: Throwable) { if (errorCallback != null ) { errorCallback.onError(e); } } } } interface ErrorCallback { void onError (Throwable e) ; } interface RunCallback { void run () ; } } class Test { public void test () { final String str = "" ; TaskUtil.doAsync(null , new TaskUtil.RunCallback() { @Override public void run () { ... Test.this .testB(str); TaskUtil.onUI(null , new TaskUtil.RunCallback() { @Override public void run () { ... Test.this .testB(str); } }); } }); } public void testB (String str) {} }
上面是一个简单的线程同步的工具类以及调用示例,可以看到,尽管简化了一部分代码,但是使用时的调用仍然非常繁琐,而且会存在上下文问题(回调函数中的this
到底是哪个this
?。那么Kotlin
里面呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 val threadPool: Executor by lazy { Executors.newCachedThreadPool() } val mainHandler: Handler by lazy { Handler(Looper.getMainLooper()) } inline fun <reified T: Any> T.doAsync ( noinline err: ((Throwable ) -> Unit )? = null , noinline run: T .() -> Unit ) { threadPool.execute { try { run.invoke(this ) } catch (e: Throwable) { err?.invoke(e) } } } inline fun <reified T: Any> T.onUI ( noinline err: ((Throwable ) -> Unit )? = null , noinline run: T .() -> Unit ) { mainHandler.post { try { run.invoke(this ) } catch (e: Throwable) { err?.invoke(e) } } } class Test { fun test () { val str = "" doAsync { testB(str) onUI { testB(str) } } } fun testB (str: String ) {} }
可以看到,对比效果非常明显。调用简单 而且结构清晰 。 那么,现在来说解释一下,上面用到了哪些东西。
顶级函数 顶级函数,也就是前面方法申明的时候,外面没有套一层Class
,减少了类的约束,成为了全局的方法。Kotlin
中的Math
类也是这样的,相比Java
中的Math
,调用时可以直接写作min(a, b)
,不用加上类名。
内联 inline 它的作用类似于我们日常中的封装,但是又和我们的封装有些不一样。我们遇到多次出现但是内容一样的代码时,一般都是抽取并且包装成一个方法,如果是多个类共用,那么可能还要再抽取一个类出来,尽管方法实现还是那样的,但是毕竟还是抽了个方法出来,可能还跨了类调用,方法寻址还是需要时间的。 而内联的作用与它类似,但是它是反过来的,当编译时,会把内联方法的代码整个复制到调用的地方,然后顺便做一下结构优化,这样就满足了我们的本来目的,一次写多份代码。
reified 这个关键一般是配合inline
使用的,从含义上来讲,是使它更真实
,我的理解就是推导泛型。 上面讲过了,编译时内联代码是以复制的形式贴到调用的地方的,而这个时候,如果方法存在泛型,那么基本上都是可以直接明确类型的,而这个关键字就是做这个的。 它可以避免泛型和类型强转引发的安全隐患,也可以实现另类的重载。
函数引用 在Kotlin
一切函数皆可lambda
,可以直接拿到函数的引用,然后在必要时调用,就像上面的run.invoke()
。 而在使用是,由于表达式的特性,直接使用大括号就好了,也省了很多事情,不用反复申明接口,new
对象。 甚至,可以通过::
来获取对象中的函数引用,只要函数,不要对象,使用和传递过程也少了很多限制。
懒加载 就是上面申明线程池的那个关键字by lazy
,它要求变量必须是val
,其实就是帮我们做了在get
的时候调用后面的表达式实例化对象,然后后面一直使用这个对象,就像帮我们做了个小单例。
糖衣内的Java 上面的糖衣看起来是真的很甜啊,可以省略很多代码,大大的提高开发的效率,那么糖衣里面是什么呢? 有个方法,在IntelliJ IDEA
中,打开一个kt
结尾的Kotlin
代码文件,然后依次打开菜单中的Tool
- Kotlin
- Show Kotlin Bytecode
。 这时,右侧会打开一个窗口,显示这个文件的字节码,再次点击窗口的Decompile
按钮,那么就会出现一个将字节码反编译为Java
的窗口了。 我们以此来了解一下语法糖
帮我们做了什么?
顶级函数 首先,我们先写一个文件,名字就叫做Test.kt
,里面放一个顶级函数,就像这样
1 2 3 4 5 package lollipop.testfun sayHello () { print("Hello" ) }
然后我们按照上面的方法,反编译一下。然后就得到了这样的一个Java
类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package lollipop.test;import kotlin.Metadata;@Metadata ( mv = {1 , 1 , 16 }, 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 = {"sayHello" , "" , "HelloWorld" } ) public final class TestKt { public static final void sayHello () { String var0 = "Hello" ; boolean var1 = false ; System.out.print(var0); } }
然后我们找一下不同,看看它帮我们做了什么。
它帮我们声明了一个类,类名就是文件名然后加上Kt
后缀,我们都知道,Java
要求每一个Java
文件都需要有一个与文件名同名的且为public
的类,显然它帮我们做了这件事。
类名和方法自动加上了final
,这也是Kotlin
在不加open
时不能被继承和重写的原因。
sayHello
方法本来是写在外面的,现在它帮我们放在了类中,同时加上了static
,说明它是一个可以直接使用的方法。
那么顶级函数就是一个静态方法吗?我们再去使用的地方验证一下。 我们创建两个文件,一个是Test.java
,另一个是Test2.kt
。 然后我们先看看在Java
中的调用.
1 2 3 4 5 6 7 8 9 10 11 12 13 package lollipop.test;class Test { private Test (String value) {} public static void with (int value) { TestKt.sayHello(); } }
经过一番尝试,发现需要按照我们刚刚反编译的方式调用, 那么Kotlin
里面的调用呢?
1 2 3 4 5 6 7 8 9 10 11 package lollipop.testclass Test2 { fun world () { sayHello() } }
这里很简单,就像本来就有这个方法一样,直接使用,看起来很舒服,那么它实际上是什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package lollipop.test;import kotlin.Metadata;@Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 1 , d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005" }, d2 = {"Llollipop/test/Test2;" , "" , "()V" , "world" , "" , "HelloWorld" } ) public final class Test2 { public final void world () { TestKt.sayHello(); } }
实际上和我们手写的Java
类是一样的调用。
伴生对象 我们都知道,在写kotlin
时,要在一个类中写一些静态方法时,一般都是写在companion object
中,中文翻译就是伴随对象
。那么它和我们的静态方法有什么关系和区别呢? 首先,我们就用上面的Test2.kt
加一点代码看看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package lollipop.testclass Test2 { companion object { const val NAME = "Test2" val pre = "I am" var id = 0 private var id2 = 1 private var id3 = 2 fun name (n: String ) : String { return "Hey $n , $pre $NAME , id is $id -$id2 " } } fun world () { id = 1 sayHello() } }
现在是这样,为了做对比,我加了三个变量一个方法,分别用不同的关键字。 现在我们来看看它的反编译结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 package lollipop.test;import kotlin.Metadata;import kotlin.jvm.internal.DefaultConstructorMarker;import kotlin.jvm.internal.Intrinsics;import org.jetbrains.annotations.NotNull;@Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 1 , d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\u0018\u0000 \u00052\u00020\u0001:\u0001\u0005B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0006" }, d2 = {"Llollipop/test/Test2;" , "" , "()V" , "world" , "" , "Companion" , "HelloWorld" } ) public final class Test2 { @NotNull public static final String NAME = "Test2" ; @NotNull private static final String pre = "I am" ; private static int id; private static int id2 = 1 ; private static int id3 = 2 ; public static final Test2.Companion Companion = new Test2.Companion((DefaultConstructorMarker)null ); public final void world () { id = 1 ; TestKt.sayHello(); } public static final void access$setId2$cp(int var0) { id2 = var0; } @Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 1 , d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\f\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u000e\u0010\u0010\u001a\u00020\u00042\u0006\u0010\u0011\u001a\u00020\u0004R\u000e\u0010\u0003\u001a\u00020\u0004X\u0086T¢\u0006\u0002\n\u0000R\u001a\u0010\u0005\u001a\u00020\u0006X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0007\u0010\b\"\u0004\b\t\u0010\nR\u000e\u0010\u000b\u001a\u00020\u0006X\u0082\u000e¢\u0006\u0002\n\u0000R\u000e\u0010\f\u001a\u00020\u0006X\u0082\u000e¢\u0006\u0002\n\u0000R\u0014\u0010\r\u001a\u00020\u0004X\u0086D¢\u0006\b\n\u0000\u001a\u0004\b\u000e\u0010\u000f¨\u0006\u0012" }, d2 = {"Llollipop/test/Test2$Companion;" , "" , "()V" , "NAME" , "" , "id" , "" , "getId" , "()I" , "setId" , "(I)V" , "id2" , "id3" , "pre" , "getPre" , "()Ljava/lang/String;" , "name" , "n" , "HelloWorld" } ) public static final class Companion { @NotNull public final String getPre () { return Test2.pre; } public final int getId () { return Test2.id; } public final void setId (int var1) { Test2.id = var1; } @NotNull public final String name (@NotNull String n) { Intrinsics.checkParameterIsNotNull(n, "n" ); return "Hey " + n + ", " + ((Test2.Companion)this ).getPre() + " Test2, id is " + ((Test2.Companion)this ).getId() + '-' + Test2.id2; } private Companion () { } public Companion (DefaultConstructorMarker $constructor_marker) { this (); } } }
我们一个个的看。
生成了一个叫做Companion
的内部类,它是静态的,并且提供了一系列方法。它应该就是我们的伴生对象了。
Companion
类在主类中有一个实例,它是同时被static final
修饰的,Kotlin
中没有静态修饰符,因此它一定程度上就代表了Kotlin
的静态方法区和静态变量区。
const
修饰的NAME
变成了一个常量,前面是public
,是一个标准的常量。
而只是少了个const
的pre
却是private
,但是在伴生对象中提供了一个getPre
方法,说明const
关键字是用来声明真·常量
。
id
初始化为0,但是在反编译代码中没有看到初始化为0的代码。
被private
修饰的其他几个变量,没有提供getter
方法。
所有声明在伴生对象中的变量对外接口几乎都是通过伴生对象,但是实际都是在主类中保存为静态的。
伴生对象中的方法,最后会在生成的伴生类中,并且是静态方法。
字符串最后生成结果仍然是用+
拼接的,而变量的使用,对于有提供getter
方法的,会使用getter
方法。
函数引用 函数式编程是Kotlin
的一大特色,这次我们看看它怎么包装的。 这次因为涉及函数的互相调用,所以改了改上面的三个类,改成了这样: Test.java
1 2 3 4 5 6 7 8 9 package lollipop.test;class Test { public static void say () { TestKt.sayHello(); } }
Test.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 package lollipop.testfun sayHello () { print("Hello" ) } fun add (num: () -> Int ) : Int { return num() + num.invoke() } fun textSay (run: () -> Unit ) { run.invoke() }
Test2.kt
1 2 3 4 5 6 7 8 9 10 11 12 package lollipop.testimport java.util.*class Test2 { fun world () { val random = Random() val value = add { random.nextInt() } textSay(Test::say) } }
Java
类没什么好看的,我们直接看后面的,Test.kt
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package lollipop.test;import kotlin.Metadata;import kotlin.jvm.functions.Function0;import kotlin.jvm.internal.Intrinsics;import org.jetbrains.annotations.NotNull;@Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 2 , d1 = {"\u0000\u0016\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0003\u001a\u0014\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00010\u0003\u001a\u0006\u0010\u0004\u001a\u00020\u0005\u001a\u0014\u0010\u0006\u001a\u00020\u00052\f\u0010\u0007\u001a\b\u0012\u0004\u0012\u00020\u00050\u0003¨\u0006\b" }, d2 = {"add" , "" , "num" , "Lkotlin/Function0;" , "sayHello" , "" , "textSay" , "run" , "HelloWorld" } ) public final class TestKt { public static final void sayHello () { String var0 = "Hello" ; boolean var1 = false ; System.out.print(var0); } public static final int add (@NotNull Function0 num) { Intrinsics.checkParameterIsNotNull(num, "num" ); return ((Number)num.invoke()).intValue() + ((Number)num.invoke()).intValue(); } public static final void textSay (@NotNull Function0 run) { Intrinsics.checkParameterIsNotNull(run, "run" ); run.invoke(); } }
可以看到,我们声明的函数
,现在变成了一个叫做Function0
的类,其他的都没变,方法内使用的是Function0
的invoke
方法,让我们看看这个类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package kotlin.jvm.functionspublic interface Function0<out R> : Function<R> { public operator fun invoke () : R } public interface Function1<in P1, out R> : Function<R> { public operator fun invoke (p1: P1) : R } public interface Function2<in P1, in P2, out R> : Function<R> { public operator fun invoke (p1: P1, p2: P2) : R } 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> { 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 }
可以看到,这是跳转到了Koltin
标准库中的一个文件,它声明了非常多的方法,最多是到了22个参数,每个都有自己的范型,而刚刚的Function0
就是无参数的接口。所以其实就是它帮我们写好了很多通用接口,然后回调触发? 那么问题来了,如果我超过22个呢?
1 2 3 4 5 6 7 8 9 10 fun args (num: (Int , String , Float , Int , String , Float , Int , String , Float , Int , String , Float , Int , String , Float , Int , String , Float , Int , String , Float , Int , String , Float ) -> Int ) : Int { return 0 }
于是我加了上面的这个方法,24个参数,看看它做了啥。
1 2 3 4 public static final int args (@NotNull FunctionN num) { Intrinsics.checkParameterIsNotNull(num, "num" ); return 0 ; }
结果是这样的,(///▽///),所以它是知道有我们这种流氓,所以干脆弄个N
的出来呗? 那么这个FunctionN
里面是啥呢?
1 2 3 4 interface FunctionN<out R> : Function<R>, FunctionBase<R> { operator fun invoke (vararg args: Any?) : R override val arity: Int }
emmm,构建者给出长度,然后参数全放数组里面,一了百了了。
行,那么我们再看看Test2.kt
,看看调用的地方怎么样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package lollipop.test;import java.util.Random;import kotlin.Metadata;import kotlin.jvm.functions.Function0;@Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 1 , d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005" }, d2 = {"Llollipop/test/Test2;" , "" , "()V" , "world" , "" , "HelloWorld" } ) public final class Test2 { public final void world () { final Random random = new Random(); int value = TestKt.add((Function0)(new Function0() { public Object invoke () { return this .invoke(); } public final int invoke () { return random.nextInt(); } })); TestKt.textSay((Function0)null .INSTANCE); } }
首先是add
方法,不出所料的是创建了一个匿名内部类,但是里面的写法很有意思,它先是实现一个final
的方法,返回值是int
的,来包裹我们的业务代码,然后另一个再去调用它。所以推测返回Object
的方法才是接口的真正方法,这样做的原因推测是跟范型擦除有点关系吧。(瞎猜的)
而另一个方法就很奇怪了,不明白使用Java
的函数引用,怎么就反编译成了这样。为了验证,我又改成了Kotlin
的方法传了进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package lollipop.testimport java.util.*class Test2 { fun world () { val random = Random() val value = add { random.nextInt() } textSay(this ::test) } fun test () { sayHello() } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package lollipop.test;import java.util.Random;import kotlin.Metadata;import kotlin.Unit;import kotlin.jvm.functions.Function0;import kotlin.jvm.internal.Reflection;import kotlin.reflect.KDeclarationContainer;@Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 1 , d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004J\u0006\u0010\u0005\u001a\u00020\u0004¨\u0006\u0006" }, d2 = {"Llollipop/test/Test2;" , "" , "()V" , "test" , "" , "world" , "HelloWorld" } ) public final class Test2 { public final void world () { final Random random = new Random(); int value = TestKt.add((Function0)(new Function0() { public Object invoke () { return this .invoke(); } public final int invoke () { return random.nextInt(); } })); TestKt.textSay((Function0)(new Function0((Test2)this ) { public Object invoke () { this .invoke(); return Unit.INSTANCE; } public final void invoke () { ((Test2)this .receiver).test(); } public final KDeclarationContainer getOwner () { return Reflection.getOrCreateKotlinClass(Test2.class ) ; } public final String getName () { return "test" ; } public final String getSignature () { return "test()V" ; } })); } public final void test () { TestKt.sayHello(); } }
这个结果看起来更加奇怪了,看样子它是做了反射,所以上面我们直接拿Java
的方法,应该也是反射了,中间也许出了点问题,不过感觉问题不大。(也许)
扩展函数 终于说到了扩展函数,Kotlin
的标准库中包含了大量的扩展函数,比如最常用的apply
和let
。 让我们看看他们是怎么样的吧。 首先是他们的声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @kotlin .internal .InlineOnlypublic inline fun <T, R> T.let (block: (T ) -> R ) : R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block(this ) } @kotlin .internal .InlineOnlypublic inline fun <T> T.apply (block: T .() -> Unit ) : T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }
这个没啥说的,只是返回值有点区别而已,我们看书看重点,看看扩展函数会变成什么吧。 代码是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package lollipop.testclass Test2 { fun test () { "hello" .let { print("" + it.length) } "world" .apply { print("" + this .length) } } }
反编译后是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package lollipop.test;import kotlin.Metadata;@Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 1 , d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005" }, d2 = {"Llollipop/test/Test2;" , "" , "()V" , "test" , "" , "HelloWorld" } ) public final class Test2 { public final void test () { String var1 = "hello" ; boolean var2 = false ; boolean var3 = false ; int var5 = false ; String var6 = "" + var1.length(); boolean var7 = false ; System.out.print(var6); var1 = "world" ; var2 = false ; var3 = false ; var5 = false ; var6 = "" + var1.length(); var7 = false ; System.out.print(var6); } }
尽管代码被inline优化过了,但是还是可以看出,它就是把被扩展的对象当作参数使用了。 这个可能看起来不明显,我们自己写个试试。 我们稍微改改代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package lollipop.testclass Test2 { fun test () { val result = "hello" .link("world" ) } private fun String.link (value: String ) : String { return this + value } }
可以看到,我们自己声明了一个扩展函数,而且没有inline
。那么反编译结果呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package lollipop.test;import kotlin.Metadata;import org.jetbrains.annotations.NotNull;@Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 1 , d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004J\u0014\u0010\u0005\u001a\u00020\u0006*\u00020\u00062\u0006\u0010\u0007\u001a\u00020\u0006H\u0002¨\u0006\b" }, d2 = {"Llollipop/test/Test2;" , "" , "()V" , "test" , "" , "link" , "" , "value" , "HelloWorld" } ) public final class Test2 { public final void test () { String result = this .link("hello" , "world" ); } private final String link (@NotNull String $this $link, String value) { return $this $link + value; } }
emmm,这次是真的非常明显了,它就是把被扩展的对象作为第一个参数,传了进去,这样一看,瞬间觉得不神奇了,索然无味啊。 PS:不过用起来是真的香。
懒加载 接着我们来看看懒加载,懒加载可以一定程度上减少了软件初始化时的爆发式内存消耗,不过它真的是这样吗?它又是怎么实现的呢?我们试试看。 我们再改改代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package lollipop.testimport java.util.*class Test2 { private val myRandom: Random by lazy { Random() } fun test () { myRandom.nextInt() } }
可以看到,我们懒加载了一个随机数对象,它会在我们使用的时候再初始化。真的是这样吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package lollipop.test;import java.util.Random;import kotlin.Lazy;import kotlin.LazyKt;import kotlin.Metadata;import kotlin.jvm.functions.Function0;@Metadata ( mv = {1 , 1 , 16 }, bv = {1 , 0 , 3 }, k = 1 , d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0005\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\t\u001a\u00020\nR\u001b\u0010\u0003\u001a\u00020\u00048BX\u0082\u0084\u0002¢\u0006\f\n\u0004\b\u0007\u0010\b\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u000b" }, d2 = {"Llollipop/test/Test2;" , "" , "()V" , "myRandom" , "Ljava/util/Random;" , "getMyRandom" , "()Ljava/util/Random;" , "myRandom$delegate" , "Lkotlin/Lazy;" , "test" , "" , "HelloWorld" } ) public final class Test2 { private final Lazy myRandom$delegate; private final Random getMyRandom () { Lazy var1 = this .myRandom$delegate; Object var3 = null ; boolean var4 = false ; return (Random)var1.getValue(); } public final void test () { this .getMyRandom().nextInt(); } public Test2 () { this .myRandom$delegate = LazyKt.lazy((Function0)null .INSTANCE); } }
可以看到,最后的类型,声明的是一个叫做Lazy
的类型,而获取的时候,是从它那里拿的,而它的初始化,是在构造器中。我们好像猜中了什么。有点似曾相识的感觉啊。 首先是Lazy
类。
1 2 3 4 5 6 public interface Lazy <out T > { public val value: T public fun isInitialized () : Boolean }
emmm,从方法名就有点感觉了,似乎就是那么回事啊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private class SynchronizedLazyImpl <out T > (initializer: () -> T, lock: Any? = null ) : Lazy<T>, Serializable { private var initializer: (() -> T)? = initializer @Volatile private var _value: Any? = UNINITIALIZED_VALUE private val lock = lock ?: this override val value: T get () { val _v1 = _value if (_v1 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST" ) return _v1 as T } return synchronized(lock) { val _v2 = _value if (_v2 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST" ) (_v2 as T) } else { val typedValue = initializer!!() _value = typedValue initializer = null typedValue } } } override fun isInitialized () : Boolean = _value !== UNINITIALIZED_VALUE override fun toString () : String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace () : Any = InitializedLazyImpl(value) }
没跑了,就是这个了,这不就是我们写单例模式
的代码吗?只不过这里用的是懒汉模式
。
总结 这里简单的看了下常用的Kotlin
的基础使用和基础的Java
实现,看起来确实是语法糖,只要合理使用设计模式,一样可以做到这样的效果,可是它有编译器支持,比我们土制
的好看啊。 所以这里,我还是建议,可以尝试把Kotlin
用起来,可以减少一大部分的低级Bug
。