一段值的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文件下,这个文件下还有LongProgressionCharProgression,结构大体类似,我们不做额外的分析。

这个类重写了iterator()方法,返回了一个IntProgressionIterator类,在同一个文件下还有LongProgressionIteratorCharProgressionIterator,分别对应于LongProgressionCharProgression类的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混用或者传入参数可为空),或者我们把rangeTodownTo方法返回的对象赋值给一个变量,这些时候编译器都会给我们创建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就能扒拉出这么多东西,大伙儿在平时使用的时候,一定要注意自己的用法,有时间可以看看字节码,总会有一些新收获。

《一段值的Kotlin之旅》 快来关注我吧!

    原文作者:小小小小小粽子
    原文地址: https://www.jianshu.com/p/7b574b343d83
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞