最近在使用kotlin,之前看好多人反对这门语言的时候都说它性能还不如Java,不过语法糖用起来是真的爽啊,因此想办法在coding过程中做一些优化,也记录下我的一些发现。
我们知道Kotlin提供了一些非常方便的方法让我们来操作集合,像map,filter。
使用起来大体如下:
val literals = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
literals.filter { it % 2 != 0 }
.filter { it > 3 }
.map { it.toString() }
我们不需要再声明多个list跟使用多个循环来得到我们想要的结果,直接一个链式调用就搞定了。很方便对吧?但是这些方法有个弊病就是他们的底层还是通过循环来实现这些功能的。比如filter的源码:
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
再看map的源码:
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}
我们可以看到底层确实都是通过循环来实现的,其他方法实现大体类似,那我们如果多次调用这些方法,只是代码可读性变高了,实际生成的字节码会调用多个循环。就拿上面例子来说,得到我们想要的结果使用了三次循环,这些循环不都是必要的,那我们可以怎么来优化?
首先最笨的方法就是尽量减少方法的调用,还是上面的例子,我们修改成:
literals.filter {
it > 3 && it % 2 != 0 }.map {
it.toString()
}
这样做确实减少了一次循环的调用,但是却不够优雅,那有没有更好的方式呢?
我在翻找文档的过程中发现Kotlin提供了一个有意思的东西:Sequence
Sequence也有一些跟集合重叠的方法,比如filter。这引起了我的好奇,我就做了个测试。测试代码如下:
@Benchmark
fun list() = (0..1_000_000)
.filter { it % 2 == 0 }
.map { it * it }
.first()
@Benchmark
fun sequence() = (0..1_000_000)
.asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.first()
测试结果有点出乎意料:
Benchmark Score Error Units
list 8843807.556 ± 75472.233 ns/ops
equence 28.591 ± 0.181 ns/op
相同的方法调用Sequence居然比list快了这么多!这Sequence到底是何方神圣?
我们先来看看Sequence的filter方法:
public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
return FilteringSequence(this, true, predicate)
}
internal class FilteringSequence<T>(
private val sequence: Sequence<T>,
private val sendWhen: Boolean = true,
private val predicate: (T) -> Boolean
) : Sequence<T> {
override fun iterator(): Iterator<T> = object : Iterator<T> {
val iterator = sequence.iterator()
var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
var nextItem: T? = null private fun calcNext() {
while (iterator.hasNext()) {
val item = iterator.next()
if (predicate(item) == sendWhen) {
nextItem = item
nextState = 1
return
}
}
nextState = 0
}
override fun next(): T {
if (nextState == -1)
calcNext()
if (nextState == 0)
throw NoSuchElementException()
val result = nextItem
nextItem = null
nextState = -1
@Suppress("UNCHECKED_CAST")
return result as T
}
override fun hasNext(): Boolean {
if (nextState == -1)
calcNext()
return nextState == 1
}
}
}
好家伙,内部居然不是我们熟悉的循环!
我不信,我们再来看看map方法:
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
internal class TransformingSequence<T, R>
constructor(private val sequence: Sequence<T>, private val transformer: (T) -> R) : Sequence<R> {
override fun iterator(): Iterator<R> = object : Iterator<R> {
val iterator = sequence.iterator()
override fun next(): R {
return transformer(iterator.next())
}
override fun hasNext(): Boolean {
return iterator.hasNext()
}
}
internal fun <E> flatten(iterator: (R) -> Iterator<E>): Sequence<E> {
return FlatteningSequence<T, R, E>(sequence, transformer, iterator)
}
}
好吧,是在下输了。这些方法底层都有一个实现了Sequence接口的类,这些类重写了iterator方法,并且还使用了从调用链前一个方法传过来的Iterator的实例,这样,每个方法在做变换时会先调用前面的方法完成变换,这样在一个循环中就能完成所有的变换!相比之前list每个方法都要循环一次的实现确实快了不少!(小声逼逼,这实现方式好像曾在RxJava中看到过)
所以大家平时在使用集合的时候,尽量先把它转换成Sequence。关于Sequence,再多说两句,其实Java8中也有类似的机制,就是Stream API,这是Java为集合准备的惰性求值的方式,Sequence的方法大概分为两组,Intermediate跟Terminal方法,像filter跟map方法都是Intermediate方法,而我们前面调用的first方法就是一个Terminal方法。当我们调用了Terminal方法之后,就会开始计算结果,此时就不能在这条调用链上调用其它的方法了。
好啦,关于集合的优化,今天就说到这里,下次,我们来聊聊Kotlin的lambda跟closure的优化。
这是一个神奇的二维码,快来关注我吧
网友评论