desehawk 发表于 2017-2-16 14:04:32

Scala入门:从HelloWorld开始【源码及编译】


问题导读

1.Scala是否运行在Java虚拟机上?
2.Scala编译后文件格式是什么?
3.如何反编译Scala?

static/image/hrline/4.gif




前言
最近在学习Scala语言,虽然还没有完全学通, 但是隐约可以体会到Scala的简洁和强大。 它既能让程序员使用函数式编程, 也提供了全面的面向对象编程。 在刚刚开始读《Scala编程》的时候, 刚读了几页, 我就被Scala语言吸引住了, 所以就一直读下去。 在学习的过程中, 也会有一些感悟, 对于一些原理, 也会尽量搞明白。 所以打算一边学习, 一边写博客, 虽然目前还没有深入, 但是还是有很多东西值得写下来。

我们知道, Scala也是一种运行于Java虚拟机上的语言, 既然能够运行于虚拟机之上, 那么它必然可以编译成class文件, 因为虚拟机只认class文件。 所以, scalac编译器将.scala源文件, 编译成class文件, 然后这些class文件被虚拟机加载并执行。


所以, 如果你对class文件格式和java虚拟机足够了解的话, 那么学习scala语言就会相对简单。Java文件编译成class文件, 而Scala源文件也是编译成class文件, 虽然他们语法大相径庭, 但是最后殊途同归。如果我们能基于class文件分析scala的行为, 有助于理解scala语言。有人说scala的语法很多很难, 其实语法终归是写法, class文件的格式是不变的, 可以把scala的语法看成java语法的高级语法糖。

本系列博客基于分析class文件, 来分析scala的语法。 如果你对class文件格式不熟悉, 建议读一下我的专栏, 该专栏是专门分析class文件和JVM行为的。 专栏地址:

http://blog.csdn.net/column/details/zhangjg-java-blog.html



Scala的HelloWorld
按照IT界的传统, 下面我们就从HelloWorld开始分析。 下面是scala版的HelloWorld源码:

object HelloWorld{
def main(args : Array){
println("HelloWorld")
}
}

如果对scala的语法不是很熟悉, 并且对scala比较感兴趣, 建议先熟悉一下scala的基本语法。 这里简单说两以下几点:

1   以object关键字修饰一个类名, 这种语法叫做孤立对象,这个对象是单例的。 相当于将单例类和单例对象同时定义。

2   方法声明以def开头, 然后是方法名, 参数列表, 返回值, 等号, 方法体 。如下:

def doSomeThing(x : Int) : Int = {
    x += 1
}

如果没有返回值, 可以省略等号, 直接写方法体。

3 Array是scala的一种数据类型, 可以理解为字符串数组。

这篇博客的目的不是详细的讲解语法, 而是基于class文件来分析scala语法的实现方式, 所以对于语法只简单提一下 。


反编译scala HelloWorld

我们所说的反编译, 是指使用javap工具反编译class文件, 所以, 在反编译之前, 要先使用scalac编译器编译该源文件:

scalac HelloWorld.scala

命令执行完成后, 可以看到HelloWorld.scala所在的目录中多出两个class文件:



其中有一个是和HelloWorld.scala对应的HelloWorld.class 。 那么HelloWorld$.class是什么呢?难道一个scala类可以生成多个class吗? 下面通过反编译来找到答案。

首先反编译HelloWorld.class :

javap -c -v -classpath . HelloWorld

反编译结果如下: (为了便于讲述, 给出了所有的输出, 会有些长)

Classfile /D:/Workspace/scala/scala-test/HelloWorld/HelloWorld.class
Last modified 2014-4-1; size 586 bytes
MD5 checksum 2ce2089f345445003ec6b4ef4ed4c6d1
Compiled from "HelloWorld.scala"
public final class HelloWorld
SourceFile: "HelloWorld.scala"
RuntimeVisibleAnnotations:
    0: #6(#7=s#8)
    ScalaSig: length = 0x3
   05 00 00
minor version: 0
major version: 50
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
   #1 = Utf8               HelloWorld
   #2 = Class            #1             //HelloWorld
   #3 = Utf8               java/lang/Object
   #4 = Class            #3             //java/lang/Object
   #5 = Utf8               HelloWorld.scala
   #6 = Utf8               Lscala/reflect/ScalaSignature;
   #7 = Utf8               bytes
   #8 = Utf8               :Q!\t\t!S3mY><vN7ea ...
   #9 = Utf8               main
#10 = Utf8               ([Ljava/lang/String;)V
#11 = Utf8               HelloWorld$
#12 = Class            #11            //HelloWorld$
#13 = Utf8               MODULE$
#14 = Utf8               LHelloWorld$;
#15 = NameAndType      #13:#14      //MODULE$:LHelloWorld$;
#16 = Fieldref         #12.#15      //HelloWorld$.MODULE$:LHelloWorld$;
#17 = NameAndType      #9:#10         //main:([Ljava/lang/String;)V
#18 = Methodref          #12.#17      //HelloWorld$.main:([Ljava/lang/String;)V
#19 = Utf8               Code
#20 = Utf8               SourceFile
#21 = Utf8               RuntimeVisibleAnnotations
#22 = Utf8               ScalaSig
{
public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic   #16               // Field HelloWorld$.MODULE$:LHelloWorld$;
         3: aload_0
         4: invokevirtual #18               // Method HelloWorld$.main:([Ljava/lang/String;)V
         7: return
}

从输出结果可以看到, 这个类确实有传统意义上的main方法。 这个main方法中的字节码指令大概是这样:

1 getstatic访问一个静态字段, 这个静态字段是定义在HelloWorld$类中的MODULE$字段, 这个字段的类型是HelloWorld$ 。 讲到这里, 大概出现了单例的影子。 我们并没有定义这个类, 所以这个类是scala编译器自动生成的, 用来辅佐HelloWorld类。

2 然后使用这个静态对象调用main方法, 这个main方法是HelloWorld$类中的, 而不是当前HelloWorld中的。 它不是静态的, 而是成员方法。


下面反编译HelloWorld$类:

javap -c -v -classpath . HelloWorld$

反编译结果如下:

Classfile /D:/Workspace/scala/scala-test/HelloWorld/HelloWorld$.class
Last modified 2014-4-1; size 596 bytes
MD5 checksum 7b3e40952539579da28edc84f370ab9b
Compiled from "HelloWorld.scala"
public final class HelloWorld$
SourceFile: "HelloWorld.scala"
    Scala: length = 0x0

minor version: 0
major version: 50
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
   #1 = Utf8               HelloWorld$
   #2 = Class            #1             //HelloWorld$
   #3 = Utf8               java/lang/Object
   #4 = Class            #3             //java/lang/Object
   #5 = Utf8               HelloWorld.scala
   #6 = Utf8               MODULE$
   #7 = Utf8               LHelloWorld$;
   #8 = Utf8               <clinit>
   #9 = Utf8               ()V
#10 = Utf8               <init>
#11 = NameAndType      #10:#9         //"<init>":()V
#12 = Methodref          #2.#11         //HelloWorld$."<init>":()V
#13 = Utf8               main
#14 = Utf8               ([Ljava/lang/String;)V
#15 = Utf8               scala/Predef$
#16 = Class            #15            //scala/Predef$
#17 = Utf8               Lscala/Predef$;
#18 = NameAndType      #6:#17         //MODULE$:Lscala/Predef$;
#19 = Fieldref         #16.#18      //scala/Predef$.MODULE$:Lscala/Predef$;
#20 = Utf8               HelloWorld
#21 = String             #20            //HelloWorld
#22 = Utf8               println
#23 = Utf8               (Ljava/lang/Object;)V
#24 = NameAndType      #22:#23      //println:(Ljava/lang/Object;)V
#25 = Methodref          #16.#24      //scala/Predef$.println:(Ljava/lang/Object;)V
#26 = Utf8               this
#27 = Utf8               args
#28 = Utf8               [Ljava/lang/String;
#29 = Methodref          #4.#11         //java/lang/Object."<init>":()V
#30 = NameAndType      #6:#7          //MODULE$:LHelloWorld$;
#31 = Fieldref         #2.#30         //HelloWorld$.MODULE$:LHelloWorld$;
#32 = Utf8               Code
#33 = Utf8               LocalVariableTable
#34 = Utf8               LineNumberTable
#35 = Utf8               SourceFile
#36 = Utf8               Scala
{
public static final HelloWorld$ MODULE$;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL


public static {};
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: new         #2                  // class HelloWorld$
         3: invokespecial #12               // Method "<init>":()V
         6: return

public void main(java.lang.String[]);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic   #19               // Field scala/Predef$.MODULE$:Lscala/Predef$;
         3: ldc         #21               // String HelloWorld
         5: invokevirtual #25               // Method scala/Predef$.println:(Ljava/lang/Object;)V
         8: return
      LocalVariableTable:
      StartLengthSlotName   Signature
               0       9   0this   LHelloWorld$;
               0       9   1args   [Ljava/lang/String;
      LineNumberTable:
      line 5: 0
}
从输出结果可以知道:


HelloWorld$类有一个静态字段

public static final HelloWorld$ MODULE$;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

它的访问修饰符是 public static final   , 类型是HelloWorld$ , 字段名是 MODULE$ 。


HelloWorld$类还有一个静态初始化方法:

public static {};
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: new         #2                  // class HelloWorld$
         3: invokespecial #12               // Method "<init>":()V
         6: return

在这个静态初始化方法中, 使用new指令创建了一个HelloWorld$对象, 并且调用该对象的构造方法<init>初始化这个对象。
实际上就是对静态字段MODULE$ 的赋值。

HelloWorld$类还有一个main方法:

public void main(java.lang.String[]);
   flags: ACC_PUBLIC
   Code:
   stack=2, locals=2, args_size=2
      0: getstatic   #19               // Field scala/Predef$.MODULE$:Lscala/Predef$;
      3: ldc         #21               // String HelloWorld
      5: invokevirtual #25               // Method scala/Predef$.println:(Ljava/lang/Object;)V
      8: return
   LocalVariableTable:
       StartLengthSlotName   Signature
            0       9   0this   LHelloWorld$;
            0       9   1args   [Ljava/lang/String;
   LineNumberTable:
       line 5: 0

这个main方法不是静态的, 是一个实例方法, 从它的字节码指令可以看出, 实现的是打印字符串HelloWorld的逻辑。


HelloWorld的实现方式总结

从上面的讲述中, 我们可知, scalac编译器使用两个class文件, 实现HelloWorld.scala源文件中的逻辑, 除了生成HelloWorld.class外, 还生产一个HelloWorld$.class 。实现逻辑如下:


1 传统意义上的入口main方法被编译在HelloWorld.class中

2在HelloWorld.class中的main方法中, 会访问HelloWorld$.class中的静态字段MODULE$(这个字段的类型就是HelloWorld$) , 并使用这个字段调用HelloWorld$中的main方法。

HelloWorld中的逻辑有点像下面这样(以下伪代码旨在说明原理, 并不符合java或scala的语法):

public class HelloWorld{
      
    public static void main(String[] args){


      HelloWorld$.MODULE$.main(args);
    }   
}
3 真正打印字符串“HelloWorld”的逻辑在HelloWorld$中。 这个类有一个main实例方法, 来处理打印字符串的逻辑, 并且该类中有一个HelloWorld$类型的静态字段MODULE$ 。 上面的HelloWorld类中的入口main方法, 正是通过这个字段调用的HelloWorld$的main实例方法来打印"HelloWorld" 。

HelloWorld$中的代码有点像这样(以下伪代码旨在说明原理, 并不符合java或scala的语法):

public final class HelloWorld${

    public static final HelloWorld$ MODULE$ = new HelloWorld$();

    public void main(String[] args){
      println("HelloWorld");
    }
}



美丽天空 发表于 2017-2-17 10:10:40

不错的资料入门

xuliang123789 发表于 2017-2-19 10:26:54

谢谢楼主,学习了,赞~~
页: [1]
查看完整版本: Scala入门:从HelloWorld开始【源码及编译】