在Scala中, 可以这样初始化一个Map对象:
这种创建Map对象的方式, 给人一种优雅的感觉, 不得不佩服Scala语言作者的想象力。 但是这种初始化的方式是如何实现的呢? ->是一个操作符吗? 还是一个方法? 如果是一个方法的话, String对象上并没有这个方法, Object对象上也没有这个方法, 那么字符串"US"是如何调用这个->方法的呢?
带着这些问题, 我们写一个实例验证一下这种初始化是如何实现的。 示例代码如下:
入口函数中只有一句代码, 这句代码以上述的方式创建一个Map对象。在之前的博客中, 我们讲述过, 以object关键字修饰的是单例对象, 这个单例对象编译成class文件之后, 会有一个虚构类。 虚构类的名字为Main$.class 。 虚构类中有一个同名的成员方法main 。 Scala入口函数的主要逻辑都在这个main方法中。 关于单例对象的实现方式, 前面有几篇文章已经介绍过了, 这里不再赘述。 不清楚的读者可以参考前面的几篇博客:
学习Scala:从HelloWorld开始
学习Scala:孤立对象的实现原理
学习Scala:伴生对象的实现原理
我们知道, 创建map对象的逻辑被编译在了Main$.class的main实例方法中。 下面我们反编译Main$.class, 看看到底是如何实现的。 下面给出Main$.class中的main方法反编译之后的字节码:
下面我们就分析main方法中的字节码, 看看到底是怎样创建Map对象的。
前两条字节码指令(索引为0和3)的意思是调用Predef$中的Map方法,该方法的返回值为scala/collection/immutable/Map,也就是说这个方法会创建一个Map对象。这里要说一句, Predef也是一个单例对象, 所以编译之后肯定有一个虚构类Predef$ 。
索引为9和10 的两条字节码指令的意思是创建一个长度为2的, 类型为scala/Tuple2的数组。
索引为21的ldc指令, 访问常量池中的字符串“US” , 根据这个常量池字符串, 创建字符串对象。
索引为23的invokevirtual指令调用Predef$中的any2ArrowAssoc 方法, 这个方法的参数是java/lang/Object, 返回值也是一个java/lang/Object 。 看到这里我们就感到奇怪了, 为什么会调用这个方法呢? 下面我们查看Predef的源码, 看看这个方法是如何实现的。相关源码如下:
所以第23行的字节码指令的意义是: 将字符串对象“US”装换成一个ArrowAssoc对象。
索引为26的ldc指令创建一个字符串对象“Washington” 。
索引为28的invokevirtual指令调用上面创建的ArrowAssoc对象的$minus$greater$extension方法。 但是我们在源码中并没有看到这个方法, 但是从名字上可以猜测出, minus代表减号- , greater代表大于号> , 所以加起来就是->方法, 所以猜测这里就是调用的ArrowAssoc中的->方法。 这个方法的调用者是由“US”包装成的ArrowAssoc对象(索引为23的指令创建的), 参数是索引为26的指令创建的字符串对象“Washington” 。 所以到此为止, 根据“US”和“Washington”创建了一个二元元组Tuple2 对象。
索引为31的aastore指令将上面创建的Tuple2 对象放入索引为10的字节码指令创建的Tuple2 数组中。
从索引32到索引50的字节码指令重复13到31的字节码指令,根据“France”和“Paris”创建一个Tuple2对象, 并放入之前创建的Tuple2 数组中。
索引为54的invokevirtual指令调用Predef$中的wrapRefArray方法, 将上面创建的Tuple2 数组对象包转成一个scala/collection/mutable/WrappedArray对象。
索引为57的invokevirtual指令调用上面创建的scala/collection/immutable/Map对象(由索引为3的字节码指令创建)的apply方法, 将上面的二元组Tuple2数组, 存放到这个scala/collection/immutable/Map对象中, 就完成了Map中数据的存储。 这个apply方法定义在scala/collection/immutable/Map的父类GenMapFactory中, 定义如下:
索引为63的astore_2指令将上面创建的Map对象保存到局部变量表中。
这个过程用Java表示的话, 是这样的(只是为了说明原理, 并不符合Java语法):