为什么不直接使用 Array 而是 IntArray ?

4,812 阅读3分钟
原文链接: mp.weixin.qq.com

基本类型居然有特殊待遇

其实我们用数组的场景不太多。有一天在做demo的时候,我突然意识到有 Array<Int> 和 IntArray 这两种完全不同的存在,当时我感觉非常的奇怪,为什么要再搞一个 IntArray 呢?不仅 IntArray,还有 DoubleArray、LongArray 等等,所有基本类型都有一个专属的 Array 版本,这背后一定有什么原因的。

偏偏巧了,kotlin-runtime 这个 jar 包里面对应的 kotlin.kotlin_builtins 这个文件没有源码,所以直接跳进去看到的 IntArray 的声明什么都没有。。


当时我就很纳闷,想来想去可能也就 IntIterator 特殊一点儿,点进去看看吧。


注意看,“without boxing” 这一句,如果大家对 Java 基本类型的装箱有所了解,看到这个肯定会立马明白什么的——Kotlin 为基本类型定制 Array 的目的是为了避免装箱?

真相总是藏在字节码里面

如果真是这个理由,那么这明显非常说得过去,毕竟装箱是有开销的。为了证实这个答案,我们只需要看下字节码就好了。我们分别用 kotlin 和 Java 写下下面的等价代码:

val intArray = intArrayOf(1,3,4)
int[] array = new int[]{1,3,4};

使用 ASM Bytecode Outline 插件查看字节码,发现二者的结果几乎是完全一样的(当然除了行号之类):


而换个方式,如果我们用 Array<Int> ,会发生什么呢?

val arrayOfInt = arrayOf(1,3,4)



这时候我们就能看到,其实是构造了一个 Integer[] 出来,每添加一个数字,就会调用 Integer.valueOf,这就产生了装箱的开销。

编译器的答案

故事本来发展到这儿就应该结束了吧,毕竟我们得到了一个说得过去的答案,可偏偏我是挖掘机专业毕业的,我就是很好奇 Kotlin 运行的时候把 IntArray 给搞成了什么样子,于是我打开了 Kotlin 的源码(1.0.5),并在 core/builtins/native/kotlin/Arrays.kt 当中找到了 IntArray 的真身:


果然,这里面的注释明确地告诉我们 IntArray 最终是要编译为 int[] 的。可是,现在骗子这么多,我怎么知道注释不是逗我玩呢?

于是我开始翻 Kotlin 的源码,主要是编译器相关的代码。关于这块儿呢,目前我还没有理解的很透彻,不过答案应该是有了的。

Kotlin 的编译器运行的时候,主要依靠一个叫做 FqName 的类来识别我们代码运行时的类型,所谓 FqName,其实就是 Full Qualified Name,简单的说就是代码元素(主要是类)的全名,比如 String -> java.lang.String。

下面是 Kotlin 映射 IntArray 的一段代码:


在编译时,Kotlin 编译器如果遇到 IntArray,就会尝试映射这个类,运行到上面的代码时,fqName 的值是 "kotlin.IntArray",而 arrayElementType 是 PrimitiveType 的一个实例 INT,而这个  PrimitiveType 又是什么呢?


是 Kotlin 基本类型的一个枚举。

我们还是看刚才的映射代码,返回的究竟是什么呢?由于我们已经知道 arrayElementType 的值,也就不难得到 JvmPrimitiveType.get(arrayElementType).desc 实际上是  I,这么说来返回的类型应该是 [I——而这个,正是 int[]的字节码类型。

小结

其实 Kotlin 中的不少类型都是对 Java 原有类型的扩展,当然有时候扩展不方便的话就直接在编译期做映射,类似的例子还有 kotlin.String -> Java.lang.String,kotlin.collections.MutableMap -> java.util.Map 等等。 如果跟高大上的设计模式、应用框架比起来,基本类型的数组问题,算不上一个“大”问题。不过,我希望通过本文传达给大家的一个信息是:“Kotlin 是开源哒,源码面前,了无秘密”。