您的位置:首页 > 其它

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

2014-04-16 18:43 691 查看
在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中为了简化语法而引入的一个特性, 叫做隐式类型转换。 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息