Kotlin的一些细节与技巧
kotlin在使用过程中有些有着一些小细节需要注意,搞清楚这些能够更加高效的使用Kotlin,下面来介绍一下。
查看字节码
kotlin本质上在编译之后还是会跟java一样生成字节码,as的工具自带查看字节码工具,能让我们看到一些舒服的kotlin操作背后的秘密 点击生成文件的Decompile 能看到kotlin文件从字节码到java代码后的结果,不过可读性并不是很好
扩展方法的小坑
Kotlin提供了扩展方法和扩展属性,能够对一些我们无法修改源码的类,增加一些额外的方法和属性 一个很简单的例子,String是JDK提供的类,我们没有办法直接修改它的源码,但是我们又经常会做一些判空、判断长度的操作,在以往使用Java的时候,我们会使用TextUtils.isEmpty来判断,但是有了Kotlin之后,我们可以像下面这样,给String定义一个扩展方法,之后在方法体中,使用this就可以方法到当前的String对象,从而实现**「看起来」**为这个类新增了一个方法的效果,如下所示
fun main() {
"".isEmpty()
}
fun String.isEmpty(): Boolean {
return this.length > 0
}
这实际上是Kotlin编译器的魔法,最终它在调用时还是以一个方法的形式,「所以扩展方法并没有真正的为这个类增加新的方法」,而只是让你在写代码时可以像调用方法一样调用工具类,来增加代码的可读性,看下它的字节码 了解这一原理之后,我们就可以理解在一些特殊case下,Kotlin的扩展为什么表现的有点不符合预期,
- 扩展类中一样签名的方法,将无效
class People {
fun run() = println("people run")
}
fun People.run() = println("extend run")
fun main() {
val people = People()
people.run()
}
//people run
因为从底层来看,类People自己的方法和扩展方法,方法签名是一样的,Kotlin编译器发现本身有这个方法了,就不会再给你做扩展方法的调用
- 扩展方法跟随声明时候类型
open class Fruit {
}
class Apple : Fruit() {
}
fun Fruit.printSelf() = println("Fruit")
fun Apple.printSelf() = println("Apple")
fun main() {
val fruit = A()
fruit.printSelf()
//注意这里
val apple1: Fruit = Apple()
apple1.printSelf()
val apple2 = Apple()
apple2.printSelf()
}
// 输出结果是
// Fruit
// Fruit
// Apple
但是第二个的输出结果却是Fruit,把apple的类型声明成了Fruit,虽然它是一个Apple的实例,但Kotlin编译器又不知道你运行时到底是什么,你声明是Fruit,就给你调用Fruit的扩展方法。
inline来帮你性能优化
在高阶函数在调用时总会创建新的Function对象,当被频繁调用,那么就会有大量的对象被创建,除此之外还可能会有基础类型的装箱拆箱问题,不可避免的就会导致性能问题,为此,「Kotlin为我们提供了inline关键字」。 inline的作用**,内联**,通过inline,我们可以把**「函数调用替换到实际的调用处」**,从而避免Function对象的创建,进一步避免性能损耗,看下代码以及 main方法的调用不再直接调用foo函数,而是把foo函数的函数体直接拷贝了过来进行调用, 不过也不能滥用inline,因为inline是在编译时进行代码的替换,那么就意味着你inline的函数体里的代码,会被替换到每一个调用的地方,从而导致字节码的膨胀,如果对产物对产物大小有严格的要求,需要关注下这个副作用。
借助reified来实现真泛型
在java中我们都知道由于编译时的类型擦除,JVM的泛型其实都是假泛型,如下的代码在编译时往往会报错
fun <T> foo() {
println(T::class.java) // 会报错
}
但是Kotlin为我们提供了**「reified关键字」,通过这个关键字,我们就可以让上面的代码成功编译并且运行,不过还需要「搭配inline关键字」**
inline fun <reified T> fooReal() {
println(T::class.java)
}
由于inline会把函数体替换到调用处,调用处的泛型类型一定是确定的,那么就可以直接把泛型参数进行替换,从而达成了「真泛型」的效果,比如使用上面的fooReal
fooReal<String>()
//调用它的打印方法时 替换为String类型
println(String::class.java)
Lateinit 和 by lazy的使用场景
这两个经常会被使用到用来实现变量的延迟初始化,不过二者还是有些区别的
- lateinit
在声明变量时不知道它的初始值是多少,依赖后续的流程来赋值,可以节省变量判空带来的便利。不过需要确保后续是会对其赋值的,不然会有异常出现
- lazy
**「一个对象的创建需要消耗大量的资源,而我不知道它到底会不会被用到」**的场景,并且只有在第一次被调用的时候才会去赋值。
fun main() {
val lazyTest by lazy {
println("create lazyTest instance")
}
println("before create")
val value = lazyTest
println("after create")
}
// before create
// create lazyTest instance
// after create
Sequence来提高性能
Kotlin为我们提供了大量的集合操作函数来简化对集合的操作,比如filter、map等,但是这些操作符往往**「伴随着性能的损耗」**,比如如下代码
fun main() {
val list = (1..20).toList()
val result = list.filter {
print("$it ")
it % 2 == 0
}.also {
println()
}.filter {
print("$it ")
it % 5 == 0
}
println()
println(result.toString())
}
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 2 4 6 8 10 12 14 16 18 20
// [10, 20]
可以看出,我们定义了一个1~20的集合,然后通过两次调用**「filter」**函数,来先筛选出集合中的偶数,再筛选出集合中的5的倍数,最后得到结果10和20,让我们看下这个舒服的fliter操作符的实现
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
可以看到,每次filter操作都会创建一个新的集合对象,如果你的操作次数很多并且你的集合对象很大,那么就会有额外的性能开销 「如果你对集合的操作次数比较多的话,这时候就需要Sequence来优化性能」
fun main() {
val list = (1..20).toList()
val sequenceResult = list.asSequence()
.filter {
print("$it ")
it % 2 == 0
}.filter {
print("$it ")
it % 5 == 0
}
val iterator = sequenceResult.iterator()
iterator.forEach {
print("result : $it ")
}
}
// 1 2 2 3 4 4 5 6 6 7 8 8 9 10 10 result : 10 11 12 12 13 14 14 15 16 16 17 18 18 19 20 20 result : 20
对于Sequence,由于它的计算是惰性的,在调用filter的时候,并不会立即计算,只有在调用它的iterator的next方法的时候才会进行计算,并且它并不会像List的filter一样计算完一个函数的结果之后才会去计算下一个函数的结果,「而是对于一个元素,用它直接去走完所有的计算」。 在上面的例子中,对于1,它走到第一个filter里面,不满足条件,直接就结束了,而对于5,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,不符合条件,就返回了,对于10,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,依然符合条件,最终就被输出了出来
Unit与void的区别
在Kotlin中,如果一个方法没有声明返回类型,那么它的返回类型会被默认设置为**「Unit」,但是「Unit并不等同于Java中的void」**关键字,void代表没有返回值,而Unit是有返回值的,如下
fun main() {
val foo = foo()
println(foo.javaClass)
}
fun foo() {
}
// 输出结果:class kotlin.Unit
继续跟进下看看Unit的实现
public object Unit {
override fun toString() = "kotlin.Unit"
}
在Kotlin中是函数作为一等公民,而不是对象。这一个特性就决定了它可以使用函数进行传递和返回。因此,Kotlin中的高阶函数应用就很广。高阶函数至少就需要一个函数作为参数,或者返回一个函数。如果我们没有在明明函数声明中明确的指定返回类型,或者没有在Lambda函数中明确返回任何内容,它就会返回Unit。 比如 如下实现实际是相同的
fun funcionNoReturnAnything(){
}
fun funcionNoReturnAnything():Unit{
}
或者是在lambda函数体中最后一个值会作为返回值返回,如果没有明确返回,就会默认返回Unit
view.setOnclickListener{
}
view.setOnclickListener{
Unit
}
Kotlin的包装类型
kotlin是字节码层面跟java是一样的,但是java中在基础类型有着 **「原始类型和包装类型」**的区别,比如int和Integer,但是在kotlin中我们只有Int这一种类型,那么kotlin编译器是如何做到区分的呢?先看一段kotlin代码以及反编译java之后的代码 可以看出
- 对于不可空的基础类型,Kotlin编译器会自动为我们选择使用原始类型,而对于可空的基础类型,Kotlin编译器则会选择使用包装类型
- 对于集合这种只能传包装类的情况,不论你是传可空还是不可空,都会选择使用包装类型
老生常谈run、let、also、with
run、let、apply、also、with都是Kotlin官方为我们提供的高阶函数,通常对比着4个操作符,
- 差异
我们关注receiver、argument、return之间的差异,如图所示
- 场景
简而言之
- **「run」**适用于在顶层进行初始化时使用
- **「let」**在被可空对象调用时,适用于做null值的检查,let在被非空对象调用时,适用于做对象的映射计算,比如说从一个对象获取信息,之后对另一个对象进行初始化和设置最后返回新的对象
- **「apply」**适用于做对象初始化之后的配置
- **「also」**适用于与程序本身逻辑无关的副作用,比如说打印日志等
==和===
在Java中我们一般使用==来判断两个对象的引用是否相等,使用equals方法来判断两个**「对象值」**是否相等 但是在Kotlin中,==和equals是相等的用来判断值,使用===来判断两个对象的引用是否相等
高阶函数
kotlin中一等公民是函数,函数也可以作为另一个函数的入参或者返回值,这就是高阶函数。 不过JVM本身是没有函数类型的,那Kotlin是如何实现这种效果的呢?先看段kotlin代码以及反编译了java的代码,一切就一目了然 我们可以看到,最终foo方法传入的类型是一个Function0类型,然后调用了Function0的invoke方法,继续看下Function0类型
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
看来魔法就在这里 也就是说如下的两种写法也是等价的
//kotlin
fun main() {
foo {
println("foo")
}
}
//java
public static void main(String[] args) {
foo(new Function0<Unit>() {
@Override
public Unit invoke() {
System.out.println("foo");
return Unit.INSTANCE;
}
});
}
到这里是不是对高阶函数有着更深刻的认识了呢 Kotlin的高阶函数本质上是通过对函数的抽象,然后在运行时通过创建Function对象来实现的。
链接:https://juejin.cn/post/7250426696346878008
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。