一段值的Kotlin之旅
我们偶尔可能会遇到需要使用一段值的场景,比如在写算法时,输入1~10,往常使用Java的时候,我们得初始化一个包含1~10的数组,我在查找Kotlin集合文档的时候,在众多的语法糖中发现了Range
类。
Range
代表了一个范围,这个范围由最大值跟最小值定义。我们来看它的用法:
val range = 1..10
没错,就是这么简单,这样我们便可以表示一到十。
当然了,它可以用来表示一大段值,比如:
val value = args[0].toInt()
when(value) {
in 100..200 -> println("Informational responses")
in 200..300 -> println("Success")
in 300..400 -> println("Redirection")
in 400..500 -> println("Client error")
in 500..600 -> println("Server error")
}
在这个情况下,用来判断HTTP状态码是不是很方便?
..
操作符对应rangeTo
方法,此处返回了一个IntRange
。注意想用它生成一段倒序的值是没有效果的,编译器也会提示我们这生成的是一个空的Range
:
val range=(3..1)//错误用法
因为range一旦发现它first > last,就不做处理了:
override fun isEmpty(): Boolean = first > last
想要一个倒序的对象我们得调用downTo
方法。除了downTo
方法最常用的就是step
方法了,step表示步长,或者说是当前值跟下一个值的差,
(1..3 step 2)//表示的范围里只有1跟3两个数
顺便我们就来好好查看IntRange
的源码,可以发现它继承自IntProgression
:
public class IntRange(start: Int, endInclusive: Int) : IntProgression(start, endInclusive, 1), ClosedRange<Int> {
override val start: Int get() = first
override val endInclusive: Int get() = last
override fun contains(value: Int): Boolean = first <= value && value <= last
override fun isEmpty(): Boolean = first > last
override fun equals(other: Any?): Boolean =
other is IntRange && (isEmpty() && other.isEmpty() ||
first == other.first && last == other.last)
override fun hashCode(): Int =
if (isEmpty()) -1 else (31 * first + last)
override fun toString(): String = "$first..$last"
companion object {
/** An empty range of values of type Int. */
public val EMPTY: IntRange = IntRange(1, 0)
}
}
而IntProgression
又实现了Iterable
接口:
public open class IntProgression
internal constructor
(
start: Int,
endInclusive: Int,
step: Int
) : Iterable<Int> {
init {
if (step == 0) throw kotlin.IllegalArgumentException("Step must be non-zero.")
if (step == Int.MIN_VALUE) throw kotlin.IllegalArgumentException("Step must be greater than Int.MIN_VALUE to avoid overflow on negation.")
}
/**
* The first element in the progression.
*/
public val first: Int = start
/**
* The last element in the progression.
*/
public val last: Int = getProgressionLastElement(start.toInt(), endInclusive.toInt(), step).toInt()
/**
* The step of the progression.
*/
public val step: Int = step
override fun iterator(): IntIterator = IntProgressionIterator(first, last, step)
/** Checks if the progression is empty. */
public open fun isEmpty(): Boolean = if (step > 0) first > last else first < last
override fun equals(other: Any?): Boolean =
other is IntProgression && (isEmpty() && other.isEmpty() ||
first == other.first && last == other.last && step == other.step)
override fun hashCode(): Int =
if (isEmpty()) -1 else (31 * (31 * first + last) + step)
override fun toString(): String = if (step > 0) "$first..$last step $step" else "$first downTo $last step ${-step}"
companion object {
/**
* Creates IntProgression within the specified bounds of a closed range.
* The progression starts with the [rangeStart] value and goes toward the [rangeEnd] value not excluding it, with the specified [step].
* In order to go backwards the [step] must be negative.
*
* [step] must be greater than `Int.MIN_VALUE` and not equal to zero.
*/
public fun fromClosedRange(rangeStart: Int, rangeEnd: Int, step: Int): IntProgression = IntProgression(rangeStart, rangeEnd, step)
}
}
这个类被包含在Progressions.kt
文件下,这个文件下还有LongProgression
跟CharProgression
,结构大体类似,我们不做额外的分析。
这个类重写了iterator()
方法,返回了一个IntProgressionIterator
类,在同一个文件下还有LongProgressionIterator
跟CharProgressionIterator
,分别对应于LongProgression
跟CharProgression
类的iterator()
方法。
我们来看看IntProgressionIterator
的源码:
internal class IntProgressionIterator(first: Int, last: Int, val step: Int) : IntIterator() {
private val finalElement = last
private var hasNext: Boolean = if (step > 0) first <= last else first >= last
private var next = if (hasNext) first else finalElement
override fun hasNext(): Boolean = hasNext
override fun nextInt(): Int {
val value = next
if (value == finalElement) {
if (!hasNext) throw kotlin.NoSuchElementException()
hasNext = false
}
else {
next += step
}
return value
}
}
很简短,跟我们常见的迭代器实现差不多,nextInt()
方法会检查下面是否还有值,设置hasNext
字段。
根据上面的分析,我们知道Range
除了继承下来的contains
等方法外,可以使用标准库为Iterable
提供的诸多扩展方法了。我们来瞎玩玩:
class Main {
fun main(args: Array<String>) {
val input = args[o].toInt()
if (input in (1..10)) {
print(input)
}
(1..10).forEach {
print(it)
}
}
}
我们来看看反编译的Java代码:
public final class Main {
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var3 = args[0];
int input = Integer.parseInt(var3);
if (1 <= input) {
if (10 >= input) {
System.out.print(input);
}
}
byte var9 = 1;
Iterable $receiver$iv = (Iterable)(new IntRange(var9, 10));
Iterator var4 = $receiver$iv.iterator(); while(var4.hasNext()) {
int element$iv = ((IntIterator)var4).nextInt();
int var7 = false;
System.out.print(element$iv);
}
}
}
啊咧,第一个判断输入是否在给定range的例子没有生成Range
对象,直接拿数值作了比较,而第二个打印出range里所有值的例子按照预期创建了IntRange
,并使用了它的iterator
来迭代。
我不敢相信自己的眼睛,我只声明一个range对象来看看:
val range = 1..2
但是结果让我更加迷糊了:
public final class Main {
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
byte var3 = 1;
new IntRange(var3, 2);
}
}
我猜,可能我们的输入是一个整型,而我们这里创建的Range
中的值也是一个整型,所以编译器又悄咪咪地帮我们做了一些事,直接省略了对象的创建转而使用最大最小值比较?而上面的代码由于我把它赋值给了一个变量,所以编译器也给我创建了对象?顺着猜想我来做验证,我把input声明成一个可能是null的变量,,我不信你编译器还能断定我输入的是一个整型:
class Main {
fun main(args: Array<String>) {
val input = args[0].toIntOrNull()
if (input in (1..10)) {
print(input)
}
}
}
这时候我们来看反编译的字节码:
public final class Main {
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Integer input = StringsKt.toIntOrNull(args[0]);
byte var3 = 1;
IntRange var4 = new IntRange(var3, 10);
if (input != null && var4.contains(input)) {
System.out.print(input);
}
}
}
果然,这时候我如愿看到了IntRange
对象的创建!果然编译器无法肯定input是一个整型数字时,它会创建Range
对象来做逻辑判断。
我又试了一下把输入值改成Double类型:
val input = args[0].toDouble()
这种情况下,编译器也会给我们创建对象:
public final class Main {
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var4 = args[0];
double input = Double.parseDouble(var4);
byte var5 = 1;
if (RangesKt.intRangeContains((ClosedRange)(new IntRange(var5, 10)), input)) {
System.out.print(input);
}
}
}
我们来比较简单情况下创建对象跟不创建对象的性能:
@State(Scope.Thread)
open class MyState {
val value = 3;
}
@Benchmark
fun benchmark1(blackhole: Blackhole, state: MyState) {
val range = 0..10
if (state.value in range) {
blackhole.consume(state.value)
}
if (state.value in range) {
blackhole.consume(state.value)
}
}
@Benchmark
fun benchmark2(blackhole: Blackhole, state: MyState) {
if (state.value in 0..10) {
blackhole.consume(state.value)
}
if (state.value in 0..10) {
blackhole.consume(state.value)
}
}
就结果来看,方法执行时间差不多:
Benchmark Mode Cnt Score Error Units
benchmark1 avgt 200 4.828 ± 0.018 ns/op
benchmark2 avgt 200 4.833 ± 0.045 ns/op
只不过其中一种多创建了对象占用了内存罢了。
到这里谜团都解开了,我们使用了一些编译器需要由iterator来实现的方法,或者range中值的类型跟拿来传入range方法的参数类型不一致时(之前Double与Int混用或者传入参数可为空),或者我们把rangeTo
跟downTo
方法返回的对象赋值给一个变量,这些时候编译器都会给我们创建Range对象,占用内存。
不管怎么说,内存能省则省,我们应当尽力避免这些情况。
最后按照惯例我们来做一下BenchMark,跟数组作比较,代码如下:
val range = 0..1_000
val array = Array(1_000) { it }
@Benchmark
fun rangeLoop(blackhole: Blackhole) {
range.forEach {
blackhole.consume(it)
} }
@Benchmark
fun rangeSequenceLoop(blackhole: Blackhole) {
range.asSequence().forEach {
blackhole.consume(it)
} }
@Benchmark
fun arrayLoop(blackhole: Blackhole) {
array.forEach {
blackhole.consume(it)
} }
@Benchmark
fun arraySequenceLoop(blackhole: Blackhole) {
array.asSequence().forEach {
blackhole.consume(it)
} }
反编译成Java大概是这样:
@Benchmark
public final void rangeLoop(@NotNull Blackhole blackhole) {
Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
Iterable $receiver$iv = (Iterable)MyBenchmarkKt.getRange();
Iterator var3 = $receiver$iv.iterator();
while(var3.hasNext()) {
int element$iv = ((IntIterator)var3).nextInt();
blackhole.consume(element$iv);
}
}
@Benchmark
public final void rangeSequenceLoop(@NotNull Blackhole blackhole) {
Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
Sequence $receiver$iv = CollectionsKt.asSequence((Iterable)MyBenchmarkKt.getRange());
Iterator var3 = $receiver$iv.iterator();
while(var3.hasNext()) {
Object element$iv = var3.next();
int it = ((Number)element$iv).intValue();
blackhole.consume(it);
}
}
@Benchmark
public final void arrayLoop(@NotNull Blackhole blackhole) {
Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
Object[] $receiver$iv = (Object[])MyBenchmarkKt.getArray();
int var3 = $receiver$iv.length;
for(int var4 = 0; var4 < var3; ++var4) {
Object element$iv = $receiver$iv[var4];
int it = ((Number)element$iv).intValue();
blackhole.consume(it);
}
}
@Benchmark
public final void arraySequenceLoop(@NotNull Blackhole blackhole) {
Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
Sequence $receiver$iv = ArraysKt.asSequence((Object[])MyBenchmarkKt.getArray());
Iterator var3 = $receiver$iv.iterator();
while(var3.hasNext()) {
Object element$iv = var3.next();
int it = ((Number)element$iv).intValue();
blackhole.consume(it);
}
}
都是一次循环迭代来完成任务。
再看看结果:
Benchmark Mode Cnt Score Error Units
arrayLoop avgt 200 2640.670 ± 8.357 ns/op
arraySequenceLoop. avgt 200 2817.694 ± 44.780 ns/op
rangeLoop avgt 200 3156.754 ± 27.725 ns/op
rangeSequenceLoop avgt 200 5286.066 ± 81.330 ns/op
这次反而是转化成Sequence之后耗时更多,不过也难免,只有一次循环迭代的情况下,Sequence的实现并没有性能上的优势。
关于Sequence的性能问题,参考这篇分析Kotlin使用优化。
我们再来一个调用多个方法的版本:
@Benchmark
fun rangeLoop(blackhole: Blackhole)
= range
.map { it * 2 }
.first { it % 2 == 0 }
@Benchmark
fun rangeSequenceLoop(blackhole: Blackhole)
= range.asSequence()
.map { it * 2 }
.first { it % 2 == 0 }
@Benchmark
fun arrayLoop(blackhole: Blackhole)
= array
.map { it * 2 }
.first { it % 2 == 0 }
@Benchmark
fun arraySequenceLoop(blackhole: Blackhole)
= array.asSequence()
.map { it * 2 }
.first { it % 2 == 0 }
来看看编译器生成的代码:
@Benchmark
public final int rangeLoop(@NotNull Blackhole blackhole) {
Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
Iterable $receiver$iv = (Iterable)MyBenchmarkKt.getRange();
Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
Iterator var5 = $receiver$iv.iterator();
while(var5.hasNext()) {
int item$iv$iv = ((IntIterator)var5).nextInt();
Integer var12 = item$iv$iv * 2;
destination$iv$iv.add(var12);
}
$receiver$iv = (Iterable)((List)destination$iv$iv);
Iterator var3 = $receiver$iv.iterator();
Object element$iv;
int it;
do {
if (!var3.hasNext()) {
throw (Throwable)(new NoSuchElementException("Collection contains no element matching the predicate."));
}
element$iv = var3.next();
it = ((Number)element$iv).intValue();
} while(it % 2 != 0);
return ((Number)element$iv).intValue();
}
@Benchmark
public final int rangeSequenceLoop(@NotNull Blackhole blackhole) {
Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
Sequence $receiver$iv = SequencesKt.map(CollectionsKt.asSequence((Iterable)MyBenchmarkKt.getRange()), (Function1)null.INSTANCE);
Iterator var3 = $receiver$iv.iterator();
Object element$iv;
int it;
do {
if (!var3.hasNext()) {
throw (Throwable)(new NoSuchElementException("Sequence contains no element matching the predicate."));
}
element$iv = var3.next();
it = ((Number)element$iv).intValue();
} while(it % 2 != 0);
return ((Number)element$iv).intValue();
}
@Benchmark
public final int arrayLoop(@NotNull Blackhole blackhole) {
Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
Object[] $receiver$iv = (Object[])MyBenchmarkKt.getArray();
Object[] $receiver$iv$iv = $receiver$iv;
Collection destination$iv$iv = (Collection)(new ArrayList($receiver$iv.length));
int it = $receiver$iv.length;
for(int var6 = 0; var6 < it; ++var6) {
Object item$iv$iv = $receiver$iv$iv[var6];
int it = ((Number)item$iv$iv).intValue();
Integer var13 = it * 2;
destination$iv$iv.add(var13);
}
Iterable $receiver$iv = (Iterable)((List)destination$iv$iv);
Iterator var15 = $receiver$iv.iterator();
Object element$iv;
do {
if (!var15.hasNext()) {
throw (Throwable)(new NoSuchElementException("Collection contains no element matching the predicate."));
}
element$iv = var15.next();
it = ((Number)element$iv).intValue();
} while(it % 2 != 0);
return ((Number)element$iv).intValue();
}
@Benchmark
public final int arraySequenceLoop(@NotNull Blackhole blackhole) {
Intrinsics.checkParameterIsNotNull(blackhole, "blackhole");
Sequence $receiver$iv = SequencesKt.map(ArraysKt.asSequence((Object[])MyBenchmarkKt.getArray()), (Function1)null.INSTANCE);
Iterator var3 = $receiver$iv.iterator();
Object element$iv;
int it;
do {
if (!var3.hasNext()) {
throw (Throwable)(new NoSuchElementException("Sequence contains no element matching the predicate."));
}
element$iv = var3.next();
it = ((Number)element$iv).intValue();
} while(it % 2 != 0);
return ((Number)element$iv).intValue();
}
看看这循环的数量,我不看结果也知道sequence系列方法完胜了。
Benchmark Mode Cnt Score Error Units
arrayLoop avgt 200 6490.003 ± 124.134 ns/op
arraySequenceLoop avgt 200 14.841 ± 0.483 ns/op
rangeLoop avgt. 200 8268.058 ± 179.797 ns/op
rangeSequenceLoop avgt 200 16.109 ± 0.128 ns/op
最后的最后,我们来做个总结,虽然都能用来表示一段值,Range
大兄弟在整体表现上是不如数组来的快,而且Range
表示的这一段值根据我们使用的方式不同,编译器最后给我们生成的表现形式也不同。编译器悄咪咪地给我们做了太多事,可能也会默默地增加我们资源的消耗,小小的Range
就能扒拉出这么多东西,大伙儿在平时使用的时候,一定要注意自己的用法,有时间可以看看字节码,总会有一些新收获。
