我們偶爾可能會遇到需要使用一段值的場景,比如在寫演算法時,輸入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
就能扒拉出這麼多東西,大夥兒在平時使用的時候,一定要注意自己的用法,有時間可以看看位元組碼,總會有一些新收穫。