您的位置:首页 > 其它

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

2016-05-24 16:46 405 查看
在Scala中, 可以这样初始化一个Map对象:

[java] view
plain copy

 





var capital = Map("US" -> "Washington", "France" -> "Paris")  

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

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

[java] view
plain copy

 





object Main {  

    def main(args : Array[String]){  

      var capital = Map("US" -> "Washington", "France" -> "Paris")  

    }  

}  

入口函数中只有一句代码, 这句代码以上述的方式创建一个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

 





public void main(java.lang.String[]);  

   flags: ACC_PUBLIC  

   Code:  

     stack=8, locals=3, args_size=2  

        0: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;  

        3: invokevirtual #23                 // Method scala/Predef$.Map:()Lscala/collection/immutable/Map$;  

        6: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;  

        9: iconst_2  

       10: anewarray     #25                 // class scala/Tuple2  

       13: dup  

       14: iconst_0  

       15: getstatic     #30                 // Field scala/Predef$ArrowAssoc$.MODULE$:Lscala/Predef$ArrowAssoc$;  

       18: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;  

       21: ldc           #32                 // String US  

       23: invokevirtual #36                 // Method scala/Predef$.any2ArrowAssoc:(Ljava/lang/Object;)Ljava/lang/Object;  

       26: ldc           #38                 // String Washington  

       28: invokevirtual #42                 // Method scala/Predef$ArrowAssoc$.$minus$greater$extension:(Ljava/lang/Object;Ljava/lang/Object;)Lscala/Tuple2;  

       31: aastore  

       32: dup  

       33: iconst_1  

       34: getstatic     #30                 // Field scala/Predef$ArrowAssoc$.MODULE$:Lscala/Predef$ArrowAssoc$;  

       37: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;  

       40: ldc           #44                 // String France  

       42: invokevirtual #36                 // Method scala/Predef$.any2ArrowAssoc:(Ljava/lang/Object;)Ljava/lang/Object;  

       45: ldc           #46                 // String Paris  

       47: invokevirtual #42                 // Method scala/Predef$ArrowAssoc$.$minus$greater$extension:(Ljava/lang/Object;Ljava/lang/Object;)Lscala/Tuple2;  

       50: aastore  

       51: checkcast     #48                 // class "[Ljava/lang/Object;"  

       54: invokevirtual #52                 // Method scala/Predef$.wrapRefArray:([Ljava/lang/Object;)Lscala/collection/mutable/WrappedArray;  

       57: invokevirtual #58                 // Method scala/collection/immutable/Map$.apply:(Lscala/collection/Seq;)Lscala/collection/GenMap;  

       60: checkcast     #60                 // class scala/collection/immutable/Map  

       63: astore_2  

       64: return  

     LocalVariableTable:  

       Start  Length  Slot  Name   Signature  

              0      65     0  this   LMain$;  

              0      65     1  args   [Ljava/lang/String;  

             64       0     2 capital   Lscala/collection/immutable/Map;  

     LineNumberTable:  

       line 11: 0  

在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

 





final class ArrowAssoc[A](val __leftOfArrow: A) extends AnyVal {  

  // `__leftOfArrow` must be a public val to allow inlining. The val  

  // used to be called `x`, but now goes by `__leftOfArrow`, as that  

  // reduces the chances of a user's writing `foo.__leftOfArrow` and  

  // being confused why they get an ambiguous implicit conversion  

  // error. (`foo.x` used to produce this error since both  

  // any2Ensuring and any2ArrowAssoc pimped an `x` onto everything)  

  @deprecated("Use `__leftOfArrow` instead", "2.10.0")  

  def x = __leftOfArrow  

  

  @inline def -> (y: B): Tuple2[A, B] = Tuple2(__leftOfArrow, y)  

  def 鈫抂B](y: B): Tuple2[A, B] = ->(y)  

}  

@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中, 定义如下:

[b][java]
 view
plain copy

 





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

 





Map map = Predef$.MODULE$.Map();  

  

Tuple2[] tArray = new Tuple2[2] ;  

  

ArrowAssoc arrowAssoc1 = Predef$.MODULE$.any2ArrowAssoc("US");  

Tuple t1 = arrowAssoc1.->("Washington");  

tArray[0] = t1;  

  

ArrowAssoc arrowAssoc2 = Predef$.MODULE$.any2ArrowAssoc("France");  

Tuple t2 = arrowAssoc2.->("Paris");  

tArray[1] = t2;  

  

map.apply(tArray);  

由此可见, Scalac编译器为我们做了大量工作。 其中有一个地方要重点强调。 那就是默认将字符串对象转成ArrowAssoc对象, 并调用->方法。 这是Scala中为了简化语法而引入的一个特性, 叫做隐式类型转换。 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: