学习Scala:Map初始化过程详解及隐式类型转换



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

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

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


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

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方法反编译之后的字节码:

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

  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 -> [B](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中, 定义如下:

def apply[A, B](elems: (A, B)*): CC[A, B] = (newBuilder[A, B] ++= elems).result

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


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


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

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中为了简化语法而引入的一个特性, 叫做隐式类型转换。 






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值