scala中的Map初始化过程详解及隐式类型转换

在Scala中, 可以这样初始化一个Map对象:

[java]  view plain  copy
 
  1. var capital = Map("US" -> "Washington""France" -> "Paris")  

这种创建Map对象的方式, 给人一种优雅的感觉, 不得不佩服Scala语言作者的想象力。 但是这种初始化的方式是如何实现的呢? ->是一个操作符吗? 还是一个方法? 如果是一个方法的话, String对象上并没有这个方法, Object对象上也没有这个方法, 那么字符串"US"是如何调用这个->方法的呢?


带着这些问题, 我们写一个实例验证一下这种初始化是如何实现的。 示例代码如下:

[java]  view plain  copy
 
  1. object Main {  
  2.     def main(args : Array[String]){  
  3.       var capital = Map("US" -> "Washington""France" -> "Paris")  
  4.     }  
  5. }  

入口函数中只有一句代码, 这句代码以上述的方式创建一个Map对象。在之前的博客中, 我们讲述过, 以object关键字修饰的是单例对象, 这个单例对象编译成class文件之后, 会有一个虚构类。 虚构类的名字为Main$.class 。 虚构类中有一个同名的成员方法main 。 Scala入口函数的主要逻辑都在这个main方法中。 关于单例对象的实现方式, 前面有几篇文章已经介绍过了, 这里不再赘述。 不清楚的读者可以参考前面的几篇博客:

学习Scala:从HelloWorld开始

学习Scala:孤立对象的实现原理

学习Scala:伴生对象的实现原理


我们知道, 创建map对象的逻辑被编译在了Main$.class的main实例方法中。 下面我们反编译Main$.class, 看看到底是如何实现的。  下面给出Main$.class中的main方法反编译之后的字节码:

[java]  view plain  copy
 
  1. public void main(java.lang.String[]);  
  2.    flags: ACC_PUBLIC  
  3.    Code:  
  4.      stack=8, locals=3, args_size=2  
  5.         0: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;  
  6.         3: invokevirtual #23                 // Method scala/Predef$.Map:()Lscala/collection/immutable/Map$;  
  7.         6: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;  
  8.         9: iconst_2  
  9.        10: anewarray     #25                 // class scala/Tuple2  
  10.        13: dup  
  11.        14: iconst_0  
  12.        15: getstatic     #30                 // Field scala/Predef$ArrowAssoc$.MODULE$:Lscala/Predef$ArrowAssoc$;  
  13.        18: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;  
  14.        21: ldc           #32                 // String US  
  15.        23: invokevirtual #36                 // Method scala/Predef$.any2ArrowAssoc:(Ljava/lang/Object;)Ljava/lang/Object;  
  16.        26: ldc           #38                 // String Washington  
  17.        28: invokevirtual #42                 // Method scala/Predef$ArrowAssoc$.$minus$greater$extension:(Ljava/lang/Object;Ljava/lang/Object;)Lscala/Tuple2;  
  18.        31: aastore  
  19.        32: dup  
  20.        33: iconst_1  
  21.        34: getstatic     #30                 // Field scala/Predef$ArrowAssoc$.MODULE$:Lscala/Predef$ArrowAssoc$;  
  22.        37: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;  
  23.        40: ldc           #44                 // String France  
  24.        42: invokevirtual #36                 // Method scala/Predef$.any2ArrowAssoc:(Ljava/lang/Object;)Ljava/lang/Object;  
  25.        45: ldc           #46                 // String Paris  
  26.        47: invokevirtual #42                 // Method scala/Predef$ArrowAssoc$.$minus$greater$extension:(Ljava/lang/Object;Ljava/lang/Object;)Lscala/Tuple2;  
  27.        50: aastore  
  28.        51: checkcast     #48                 // class "[Ljava/lang/Object;"  
  29.        54: invokevirtual #52                 // Method scala/Predef$.wrapRefArray:([Ljava/lang/Object;)Lscala/collection/mutable/WrappedArray;  
  30.        57: invokevirtual #58                 // Method scala/collection/immutable/Map$.apply:(Lscala/collection/Seq;)Lscala/collection/GenMap;  
  31.        60: checkcast     #60                 // class scala/collection/immutable/Map  
  32.        63: astore_2  
  33.        64return  
  34.      LocalVariableTable:  
  35.        Start  Length  Slot  Name   Signature  
  36.               0      65     0  this   LMain$;  
  37.               0      65     1  args   [Ljava/lang/String;  
  38.              64       0     2 capital   Lscala/collection/immutable/Map;  
  39.      LineNumberTable:  
  40.        line 110  

在Scala源码中, 一句创建Map对象的代码竟然对应class文件中的29条字节码。 这真实太神奇了, 编译器给我们做了大量的工作, 简化了我们的编码任务, 但是提高了学习门槛, 我们必须明白编译器额外为我们做了哪些工作, 才能对Scala理解的比较深入。 就像《Scala编程》一书的作者再书中说的那样: 一边情况下你不必知道编译器做了什么, 但是有时候掀开盖子看看下面有什么, 能加深我们的理解(大概意思是这样, 原话不记得了)。 


下面我们就分析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的源码, 看看这个方法是如何实现的。相关源码如下:

[java]  view plain  copy
 
  1. final class ArrowAssoc[A](val __leftOfArrow: A) extends AnyVal {  
  2.   // `__leftOfArrow` must be a public val to allow inlining. The val  
  3.   // used to be called `x`, but now goes by `__leftOfArrow`, as that  
  4.   // reduces the chances of a user's writing `foo.__leftOfArrow` and  
  5.   // being confused why they get an ambiguous implicit conversion  
  6.   // error. (`foo.x` used to produce this error since both  
  7.   // any2Ensuring and any2ArrowAssoc pimped an `x` onto everything)  
  8.   @deprecated("Use `__leftOfArrow` instead""2.10.0")  
  9.   def x = __leftOfArrow  
  10.   
  11.   @inline def -> [B](y: B): Tuple2[A, B] = Tuple2(__leftOfArrow, y)  
  12.   def 鈫抂B](y: B): Tuple2[A, B] = ->(y)  
  13. }  
  14. @inline implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] = new ArrowAssoc(x)  

可以看出, 这个any2ArrowAssoc 方法将传入的对象x包装成一个ArrowAssoc对象, 这个对象是Predef的内部类, 从上面的代码中可以看到, 这个类中有一个叫做 ->的方法 。 这个方法根据传入的参数创建一个二元元组Tuple2 。 

所以第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中, 定义如下:

[java]  view plain  copy
 
  1. def apply[A, B](elems: (A, B)*): CC[A, B] = (newBuilder[A, B] ++= elems).result  

到此为止, Map对象就创建完了, 并且也把数据存到了Map对象中。 


索引为63的astore_2指令将上面创建的Map对象保存到局部变量表中。 


这个过程用Java表示的话, 是这样的(只是为了说明原理, 并不符合Java语法):

[java]  view plain  copy
 
  1. Map map = Predef$.MODULE$.Map();  
  2.   
  3. Tuple2[] tArray = new Tuple2[2] ;  
  4.   
  5. ArrowAssoc arrowAssoc1 = Predef$.MODULE$.any2ArrowAssoc("US");  
  6. Tuple t1 = arrowAssoc1.->("Washington");  
  7. tArray[0] = t1;  
  8.   
  9. ArrowAssoc arrowAssoc2 = Predef$.MODULE$.any2ArrowAssoc("France");  
  10. Tuple t2 = arrowAssoc2.->("Paris");  
  11. tArray[1] = t2;  
  12.   
  13. map.apply(tArray);  

由此可见, Scalac编译器为我们做了大量工作。 其中有一个地方要重点强调。 那就是默认将字符串对象转成 ArrowAssoc对象, 并调用->方法。 这是Scala中为了简化语法而引入的一个特性, 叫做隐式类型转换。 

你可能感兴趣的:(scala中的Map初始化过程详解及隐式类型转换)