一段值的Kotlin之旅

作者: 小小小小小粽子 | 来源:发表于2019-03-28 23:08 被阅读45次

    我们偶尔可能会遇到需要使用一段值的场景,比如在写算法时,输入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.haomeiwen.com/subject/nuihbqtx.html