前言
最近在學習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[String]){println("HelloWorld")}}
如果對scala的文法不是很熟悉, 並且對scala比較感興趣, 建議先熟悉一下scala的基本文法。 這裡簡單說兩以下幾點:
1 以object關鍵字修飾一個類名, 這種文法叫做伴生對象,這個對象是單例的。 相當於將單例類和單例對象同時定義。
2 方法聲明以def開頭, 然後是方法名, 參數列表, 返回值, 等號, 方法體 。如下:
def doSomeThing(x : Int) : Int = {x += 1}
如果沒有返回值, 可以省略等號, 直接寫方法體。
3 Array[String]是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_SUPERConstant 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_SUPERConstant 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: Start Length Slot Name Signature 0 9 0 this LHelloWorld$; 0 9 1 args [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: Start Length Slot Name Signature 0 9 0 this LHelloWorld$; 0 9 1 args [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");}}
本文基於分析class位元組碼來分析scala, 對class檔案格式不熟悉的同學, 可以參考我的專欄: 深入理解Java語言 。