2011年5月7日 星期六

5-8. Abstract class

Abstract class 代表的是該 class 是一個抽象的 class,所以不能把該 class 直接 create instance。
Abstract class 通常會包含多個 abstract method,但也可以沒有 abstract method。沒有 abstract method 的 abstract class,通常代表該 class 不是 concrete class,希望使用 subclass 來 create instance。

比如我們有一個 Animal class,但在我們應用中,Animal 只是用來表明所有的動物的 superclass,Animal 並沒定義有 abstract method,此時我們仍會將 Animal 標注為 abstract class。
所以 abstract class 主要是要看它是否代表抽象而言,而非以有無 abstract method 來斷定。當然有 abstract method 的 class 就一定是 abstract class。

在 Java 中,abstract class 需要在 class 前以 abstract modifier 註明該 class 是 abstract class。而 abstract method 也需要以 abstract modifier 註明該 method 是 abstract method。
範例
abstract class C { // 用 abstract 註明本 class 為 abstract class
  abstract void m1(); //用 abstract 註明本 method 為 abstract method
}
當有 subclass 繼承某個 abstract class 時,需要 implements 所有的 abstract method,否則表示該 subclass 仍有尚未 implement 的 abstract method,這時該 subclass 仍需宣告為 abstract class。

Scala 的 abstract class 的概念與 Java 的類似。也是使用 abstract 宣告。
但有點不同之處,Scala 中不需特別宣告 abstract method,也不能宣告某個 method 為 abstract method。只要有某些 method 沒有定義,那些 method 自動會被 Scala 標示為 abstract method。

所謂 method 沒有定義的意思是,method 宣告後沒有對應的程式碼(即沒有使用等號,或 method 後面沒有使用大括號括住的程式碼),就是 method 沒有定義。

範例
abstract class S { // 用 abstract 註明本 class 為 abstract class
  def p: String // 宣告一個 method p,但沒有定義,所以 p 是 abstract method
}
val s = new S // 這是錯誤的,因為 abstract class 不能 create instance

5-7. Anonymous class

Anonymous class 就是不具名的 class,通常用在我們不需要給一個很清楚的 class 名稱時。

在 Java 我們時常遇到 anonymous class,anonymous class 讓我們很方便產生一個暫時性的 class。
Java 產生 anonymous class 的語法,通常隨著 create 一個 anonymous class 的 instance 一起。
範例
Runnable r = new Runnable { //這裡表示產生一個 anonymous class,但implements Runnable interface
                            //大括號裡面就是該 anonymous class 的內容
                            //定義完 anonymous class,馬上 create 一個 instance 傳回去
      public void run() {
         ...
      }
    };
Thread thrd = new Thread(r).start();
  });
或許語法,你不會很熟悉,但這是很常見的寫法,請把它記住。

在 Scala 中,anonymous class 的用法更多,通常也是跟隨著要產生一個特定型態的 instance。
Scala 中的語法與 Java 的類似。
範例
val v = new { // new 後面不需要有 type 名稱,直接可以宣告 anonymous class 並 create instance
          def p = println("P")
        }

Anonymous class 一個很好用的工具,雖然導致出來的語法好像有點掉頭掉尾,但我們應該好好使用,可以讓你得成是寫起來更快速,更精簡。

5-6. Inner class

Inner class 在 Java 中時常被使用,在 Scala 中也一樣很常被使用。

Java 宣告 inner class 的方式,是在 class 中 宣告另一個 class。
Inner class 被編譯後,會產生 outer_class$inner_class 的 classfile。
甚至在 method 中也可以宣告 inner class。

範例:宣告 inner class
class J1 {
  class J11 { //inner class
    void print() {
      System.out.println("J11");
    }
  }
}
上例會產生兩個 class file
1. J1.class
2. J1$J11.class:代表 J11 的 inner class。

範例:在 method 中宣告 inner class,此時只有該 method 才知道這個 inner class。
class J1 {
  void p1() {
    class J11 { //method 中 的 inner class
      void print() {
        System.out.println("p1");
      }
    }
  }
  void m1() {
    J11 j11 = new J11();//這裡是錯誤的,因為 m1() 看不到 J11
  }  
}
上例會產生兩個 class file
1. J1.class
2. J1$J11.class:代表 J11 的 inner class。
但由於 J11 是宣告在 J1.p1() method 裡面,所以在 m1() 中將看不到 J11 這個 class。

Inner class 的 instance 隱含有一個 outer class instance 的 reference。也就是,若要 create 一個 inner class instance,需要有一個 outer instance 才能成功 create。若在其他 class 要 create inner class 的 instance,需要使用特殊的語法。
範例:inner class instance 的 creation。
class J1 {
  class J11 { //inner class
    void print() {
      System.out.println("J11");
    }
  }
  void m1() {
    J11 j11 = new J11();//這裡 create 一個 J11,好像沒有 outer class instance
//但其實在 instance method 中呼叫,已經隱含一個 outer class instance。
  }
  static public void main(String[] args) {
    J11 j11 = new J11();//這裡會出錯,因為 J11 的 instance 需要 outer class instance
    J1 j1 = new J1();
    J11 j11 = j1.new J11();//這裡使用 j1 來 create J11。
//請注意 j1.new J11() 這個特殊語法
  }
}

Scala inner class 用法,與 Java 的類似。我們看例子。
範例:宣告 inner class
class S1 {
  class S11 {
    def p1 = println("p1")
  }
}
上例會產生兩個 class file
1. S1.class
2. S1$S11.class:代表 S11 的 inner class。

請注意,Java 的 inner class 的實際 type,為:outer_class.inner_class,使用「.」dot。
但 Scala 的「.」dot,只能使用在 instance 上。compiler 一遇到 dot 就會試圖解釋為 instance 的 method 或data field。
所以,在 Scala,inner class 的實際 type,為 outer_class#inner_class。Scala 與 Java 不太相同,請注意。
class S1 {
  class S11 {
    def p1 = println("p1")
  }
}
object Main {
  def m1 {
    val s11: S1.S11 = null // 使用 S1.S11 當 type,但這是錯誤的。遇到「S1.xxx」時,compiler 會去找 S1 這個 object
    val s12: S1#S11 = null // S1#S11 才是正確的 type。
  }
}
範例:在 method 中宣告 inner class,Scala 的用法與 Java 相同。
class S1 {
  def p1 = {
    class S11 { // 在 method 中宣告 inner class
    }
  }
}
與 Java 相同,Scala inner class 的 instance,也是隱含一個 outer class 的 reference。所以要 create 一個 inner class 的 instance,需有一個 outer instance。
使用 outer instance 來 create inner instance 的語法,Scala 與 Java 不太相同。
class S1 {
  class S11 // 這個 class 沒有 body,所以可以省略大括號
}
class Main {
  val s1 = new S
  val s11 = new s1.S11 // 這裡的語法與 Java 不同,new 為 keyword,放在最前面
}
在 Scala 中,不同的 outer instance 所產生出來的 inner instance,type 是不相同的。
讓我們看下一例子。
class S {
  class Inner;
}
class Main {
  val s1 = new S
  val s1_inner = new s1.Inner  //s1_inner 的 type 為 s1.Inner
  val s2 = new S1
  val s2_inner = new S2.inner //s2_inner 的 type 為 s2.inner
}
上例的 s1_inner 與 s2_inner 都是 S#Inner 的 type。
但實質上,二者的 type 並不相同,一個是 s1.Inner type,另一個是 s2.inner type。這種與 outer instance 相關的 type,稱為 path-dependent type。

5-5. package

Java 使用 package 來組織我們的程式。我們將程式區分成各個 module,然後依照彼此相依的程度,放到不同的 package 中。Java 使用 package 來命名不同的 namespace。

使用 package 的主要用意有兩個
1. 組織我們的程式碼
2. 避免名稱衝突:比如你自己的程式有個 class A,你使用的 library 也有個 class A,那就麻煩。

Java 建議使用 domain name 的倒裝來命名你的 package。比如,昇陽的 domain name 為 sun.com,所以會以 com.sun.xxx 來當成 package 名稱。

基本上,domain name 倒裝的建議是滿好的,但卻會造成 package name 過長的情況。比如,我們常用的 Apache Log4J,它的 configure 的  package 名稱為 org.apache.log4j.config,夠長。你會希望你自己公司內部的 Java 程式使用這麼長的 package name?因此,通常,我們並不一定會如此命名。

其實,domain name 倒裝主要的因素是為避免名稱衝突,所以一般用在會 release 給不特定用戶的 library 上。若是你的程式只是自己公司內部使用,該程式不準備 release 給別人使用,這時,並不需要如此嚴謹,僅需要按照部門或是其他功能別,做適當的 package 命名即可。

關於 Java 的 package,有一點可能有一些人誤會,那就是 package 與 sub-package 的關係。比如「org.apache.log4j」與「org.apache.log4j.config」,我們有時會說後者是前者的 sub-package。
但,說 sub-package 其實並不合適,因為 Java 對於所謂的 package 與 sub-package 二者是完全無關的,也就是 p1 ,與 p1.p2 兩者可以視為是完全獨立的 package。p1 與 p1.p2 的關係,就如同 pa 與 pb 之間的關係一樣,兩者是完全獨立的,沒有所謂的 p1.p2 繼承 p1 的這件事,請大家要小心。
這個原則不僅在 source code 上有效,在編譯完後的 classfile 也同樣是成立的。

Java 使用 package 這個 keyword 宣告該檔案裡面所有 class 歸屬的 package。
要注意的是, Java 中的 package keyword 需要是該檔案的第一個有效的 statement,否則 compiler 會視為錯誤。Java 檔案只能有一個 package 的 statement。

範例:Java 中使用 package 宣告 package name。
package p1.p2;//宣告本檔案屬於 p1.p2 package
class J1 ...
class J2 ...

Scala 延續 Java package 的概念,所以我們可以在 Scala 繼續使用 package 的概念,但 Scala 稍微做了一些的修改。

在 Scala,我們可以繼續使用 Java package 的宣告方式,使用這種方法,在該檔案裡面宣告的 class 都隸屬在該  package 裡面,與 Java 的使用方式一模一樣。
範例:Scala 中使用 Java 方式的 package 宣告。
package p1.p2
cass S1 ...
class S2 ...

Scala 的 package 命名,也可使用大括號將 package 的範圍刮起來,這種方式稱為 packaging。
範例:Scala 中使用 packaging 的方式宣告 package。
package p1.p2 { // 使用大括號,宣告 p1.p2 的範圍
  class S1
  class S2
}

Scala 的檔案,可以允許有多個 package,也可以在 package 中宣告 sub-package。
範例:一個檔案中有兩個 package
package p1 { // 宣告 p1 的範圍
  class S1
}
package p2 { // 宣告 p2 的範圍
  class S2
}
範例:package 裡面宣告 sub-package
package p1 { // 宣告 p1 的範圍
  class S1 // S1 是在 package p1 裡面
  package p2 {
    class S2 // S2 是在 package p1.p2 裡面
  }
}
範例:package 裡面宣告 sub-package,若裡面只有一個 sub-package,可以省略大括號
package p1 // p1 裡面只有一個 p2,所以大括號省略
  package p2 {
    class S2 // S2 是在 package p1.p2 裡面
  }
範例:package 裡面宣告 sub-package,若省略大括號,可以將 sub-package 往左移動,會比較好看
package p1 // p1 裡面只有一個 p2,所以大括號省略
package p2 { // p2 往左移,看起來猶如連續宣告兩個 package,這種連續宣告 package 的方式,稱為 chained-package clauses
  class S2 // S2 是在 package p1.p2 裡面
}

在上例這種 package 中,包含 sub-package 的宣告方式,Scala 允許 package 與 sub-package 間有更彈性的使用方式。
但請注意,這只是 Scala 的 syntax sugar,就如同前面提的,編譯後的 package 與 sub-package 彼此完全獨立。所以或許使用 inner-package 的名稱會比 sub-package 的名稱好,以下將使用 inner-package 這樣的名稱。
範例:inner package 可以直接使用 outer-package 的 class
package p1 {
  class C1
  package p2 { //這個 package 實際代表 p1.p2
    class C2 {
      val c1 = new C1 // inner-package 的 class,可以使用 outer package 的 class,而不需使用 full-path class name
      val c11 = new p1.C1 // 此為完整的 access 方式,上一個 statement 為 syntax sugar
    }
  }
}
範例:outer package 可以使用相對路徑 access inner-package 的 class
package p1 {
  class C1 {
    val c2 = new p2.C2 // 使用相對路徑 access inner package 的成員,此仍為 syntax sugar
    val c22 = new p1.p2.C2 // 此為完整的 access 方式
  }
  package p2 { //這個 package 實際代表 p1.p2
    class C2
  }
}
範例:若 inner package 與 outer package 分開寫,syntax sugar 不再啟動,需使用絕對路徑 access 方式
package p1 {
  class C1 {
    val c2 = new p2.C2 // 使用相對路徑,這是錯誤的
    val c22 = new p1.p2.C2 // 此為完整的 access 方式
  }
}
package p1.p2 {
    class C2 {
      val c1 = new C1 //使用相對路徑,這是錯誤的
      val c11 = new p1.C1 // 此為完整路徑,這裡才是正確的
    }
  }
上例中,package p1 與 p1.p2 回復 Java 原有的規則,彼此之間互相獨立,所以彼此都需要使用絕對路徑來 access。

Scala 定義一個東西,用來表示最外層的 package,此為 _root_,所有 top-level 的 package 都是_root_的 sub-package。
注意:_root_ 只能用來 access 最上層的 package,但不能用來 access 最外層 class。
範例
package p1 {
  class C1
  package p2 {
    class C2 {
      val c1 = new C1 //使用相對路徑,使用到 p1.C1
      val c1_top = new _root_.C1 // 使用 _root_ access 來 access 最外層的 class C1,錯誤,因為 C1 不是 _root_ 的 element
      val c1_top2 = new _rrot_.p2.C1 // 使用 _root_ access p2.C1
    }
  }
}
class C1
package p2 {
  class C1
}
上例中,我們希望在 p1.p2.C2 access 最外層的 p2.C1,此時需要使用 _root_。
但 _root_ 無法用來 access 最外層 package 的 class。

看到這,你是否覺得 Scala 的 package 機制已經過於複雜。若你的程式在這種想細節上玩把戲,筆者認為是不智的。
使用 package 包 inner package,再加上使用相對路徑的方式,只會讓你的頭腦變暈,建議不要這麼複雜,回歸 Java 一個 package 的寫作方式,若有需要運用別 package 的 element,再使用 import statement 就好了。當然一個檔案包含多個 package 的作法到是不需丟棄,可以好好運用。

Scala 雖然對 package 做了修正,但對於 Java 對 package name 使用 domain name 倒裝的建議,仍然維持。

5-4. 檔案與 class 的關係

在 Java 中,source code 的檔案名稱,以及目錄階層,與你的 class name 與 package 需要相對應。這是你在撰寫 Java 程式時需要注意的。

Java 規定 public class 需要放在與 class 同名的檔案中,目錄結構需要與 package 結構相同。

這樣規定的好處,是可以一目了然,看出原始碼的結構。但壞處是,常常我們會發現,Java 的檔案數很多,目錄層次也很多。最常感到無奈的是,你無法在一個 Java 原始檔案中看到兩個 public class,常常為了一個新的 public class,需要再產生一個 Java 檔案。

不僅僅 source code,甚至所編譯出來的 class file,Java 也會按照 package 的方式,將 class files 放置到對應 package 的目錄中。JVM 因此可以按照目錄,找尋到所需要的 class。

這種與檔案結構相對應的構想,有好也有壞,我們不適合去評判。

在 Scala 中,已將這樣的規定移除。在 Scala 程式中,你可以在一個檔案中宣告多個 class,甚至這些 class 是不同的 package。Package 命名也不再與目錄層次相對應。

將這種規定,讓人有鬆綁的感覺,確實適合 Scala 宣傳其快速開發的格調,但好像就比較不嚴謹。

可是雖然 Java 嚴謹,但這樣命名的規定,相信大多數人卻也看不出實質好處在何處。所以,Scala 移除這樣的規定,對大部分人而言,應該是好事一樁。

雖然,Scala 把 class 命名與檔案目錄對應的規定移除,但這只是 source code 的部份,對於編譯出來的 classfile,Scala 仍依循 Java 的規定,否則 JVM 該如何找到所需的 class 呢?這是你該曉得的。

5-3. Instantiation

有了 class 的宣告,如何 create 該  class 的 instance?
這個 create instance,在 OO 的術語叫做 instantiation,相信你知道。

Java 中,要產生一個 class 的 instance,使用 new class_name(param...) 的方式來產生 instance。
Scala 的方式,與 Java 一模一樣,也是 new class_name(param...)。
其中的 param 是參數,表示要傳給 constructor 的參數列。
例:
val s1 = new S() // create 一個 S instance, constructor 沒有參數
val s2 = new S(1, "abc") //// create 一個 S instance, constructor 的參數為 1 與 "abc"
Scala 有一點點與 Java 不同的,就是當 constructor 沒有參數時,可以省略小括號。
這個我們可以簡稱為「小括號省略原則」。
「小括號省略原則」,我們在討論 method 呼叫時,也會看到,這是一個相當重要的原則。

例:
val s = new S // 當 S 的 constructor 不需要有參數時,可以省略小括號

對 Java 熟悉的你,相信 instantiation 不會是件困難的事。

5-2 . Scala object hierarchy

我們首先來談,OO 語言中,最基礎的 object 的架構。

Scala 與 Java 一樣,為避免多重繼承所引起的問題,以及簡化語言的複雜性,採取單一繼承的方式,這也是許多程式語言的方式。

其實,更嚴格來講,不是 Scala 採取與 Java 相同的單一繼承方式,而是「Scala 繼續使用 Java OO 的架構」,這樣講更精確。因為 Scala 根本沒有離開 Java 的範疇。

之前,我們曾提過,Scala 可以與 Java 混用,混用的基礎,除了 Scala 編碼出 JVM 的 bytecode 外,更重要的是,Scala 繼續沿用 Java OO 架構。
我們可以想像「Scala 是 Java 的另一種語法」,這句話是確實的,而且讀者應該把它聽進去。Scala 在 Java 原來架構中擴充,以 JVM 的角度,兩者是混雜在一起。以程式語言的角度, Scala 只是擴充 Java。

我們來看如何擴充?

Java 除了 object 外,為了效能,也支援 8 種支援 primitive type。所以,在 Java 你需要同時面對 primitive type value 與 object。
下圖,是 Java 關於資料型態的示意圖。



我們提過,Scala 為純 OO 的語言。但這不代表 Scala 的效能就不好,或是 Scala 去除掉 Java 的 primitive type。

Scala 是 Pure OO 語言,所以有一套完整的繼承體系。為兼顧效能的問題,Scala 特別為值相關的整合在 Value 值的子系中,而將一般非值的放入 Reference 的子系。Compile 時 compiler會特別為值子系的 instance 編譯為 JVM 對應的資料。

因此,Scala 的手法是包裝,把 Java 的 primitive type 使用 class 把它包裝起來,讓這些 primitive type value,在 object hierarchy 中出現在該出現的地方。

在 Scala 中,把所有的 primitive type 使用一種特殊的 class 包裝起來,這個特殊的 class,在 Scala 稱為 value class。所有 Java 的 primitive type,在 Scala 中都有對應的 class。Value class 有一個共同的 parent class,稱為AnyVal。也就是 AnyVal 是所有 value class 的 root class。

但請注意,這些 value class 是 Scala 世界才可以看到,compile 完後,會變成 Java 的 primitive type,因此不會有效能的問題。我們可以說這些 value class,對應到 Java 的 primitive type。

Scala 的 value class,有一個較特殊的,叫 Unit,我們之前曾經討論過。Unit 是 Java 的 void 對應,但它是一個 value class,請大家不要搞混了,以為它也是一個 reference class。

同樣的,Java 的 Object class,在 Scala 中有一個對應的 class 叫做 AnyRef,代表所有 reference 的 root class。你可以想像,Java 的 Object 就是 AnyRef。在 Scala 世界,Object 這個 class 不再是 object hierarchy 的 root class。

看到此,我們知道,Scala 將 object hierarchy 分成兩大支,一個是 AnyVal,一個是 AnyRef。很顯然,Scala 已經將原來的 object hierarchy 架構擴充開來。AnyRef 這個系列就是原來 Java 的 object hierarchy,而 AnyVal 這個系列就是原來 Java 的 primitive type(再加上 void)。

為整合 AnyRef 與 AnyVal 這兩個大的子系,有一個 Any 的 class 出現,這個 class 是 AnyVal 與 AnyRef 的 parent class,這個 Any 才是 Scala 中的 root class。

上面我們使用「Scala 的什麼東西,對應 Java 的什麼東西」,「對應」這兩個字,其實要更強化一點,對應的意義其實是「把它們當成相同」,我們再進一步解釋:
1. Java 的 int,在 Scala 使用 Int 對應:這代表「Scala 的 Int 就是 Java 的 int,只不過 Scala 把它包裝起來」。同理,所有的 primitive value class 都可這樣看待。
2. Java 的 void,在 Scala 使用 Unit 對應:這代表「Scala 的 Unit 就是 Java 的 void,只不過 Scala 把它包裝起來」。
3. Java 的 Object,在 Scala 使用 AnyRef 對應:這代表「Scala 的 AnyRef 就是 Java 的 Object,兩者可視為同義」

最後我們將 Scala object hierarchy 整理如下圖:



在上圖,可以發現有兩個 class,Null 與 Nothing。Scala 把這兩個 class 加入的原因,是要將 object hierarchy 弄得更完整。

在 OO 語言中,parent class 的變數,可以使用 subclass 的 instance 來指定。所以在 Java,我們可以將任何 instance 指定給 Object 變數,比如,下例是合法的。
Object obj = "abc"; // obj 為 Object 類型的變數,可以使用 "abc" 指定

在 Java,我們可以把 null 值設給 Object 變數。在 Java 中,null 被當成很特殊的 instance 值,所以我們可以「把 null 值設給 Object 變數」,這算是一個特殊規定。

Scala 不希望依循特殊規定, Scala 希望依循的是 OO 的規範。若要達到「讓所有 AnyRef 的變數可以設定為 null」,則 null 必須有某種特性在。注意,在 Scala 中所有東西都是 object,null 也是一個 object。

「讓所有 AnyRef 的變數可以設定為 null」,表示 null 必須是一個特殊 class 的 instance,而該 class 是所有 AnyRef class 的 subclass。

為符合這樣 assignment 的原則,Scala 定義一個新的 class 叫 Null,這個 Null class 是所有 AnyRef subclass 的 subclass。Null class 只有一個 instance,叫做 null。如此一來,null 可以很順利的指定給任何 AnyRef 的變數,而不會破壞 OO 的理論。

請注意,Scala 的 null,不像在 Java 中是一個值,在 Scala 中,它是一個 instance。

Null 是所有 AnyRef subclass 的 subclass,Scala 也針對 Any 這個 class 定義了一個 class,叫 Nothing class,該 class 是所有 Any subclass 的 subclass,這樣整個 Scala 的 object hierarchy 就完整了。
但請注意,Nothing 沒有 instance,因為 AnyRef 的變數 可設定為 null,但 AnyVal 的變數一定是有值,所以不需設為 nothing 這樣的東西,所以 Nothing 並沒有存在 instance 的必要。

Null 與 Nothing 稱為「底層型態」(Bottom Type)。

我們稍微整理一下,Scala 繼續使用 Java 的 object hierarchy,並沒有改變。但 Scala 將原來 Java 的 primitive type 與 object type 加以整合,讓你可以使用純 OO 的角度來看 Scala 的世界。

但請注意,這些擴充與整合,都只出現在 Scala 的世界,並不因此改變 Java 的東西,Java 的東西完全沒有做任何更改。
當 compile 完成,這些 Scala 所擴充的出來的東西,都會編譯為原來 Java 該有的面貌,完全不會改變 Java 的世界。

由於 Scala 只是延續 Java 的架構,並沒有破壞 Java 的東西,所以 Scala 可以與 Java 混合得這麼成功,也因此,我們常說,我們可以把 Scala 看成開發 Java 程式的另一個方法。