面向 Java 開發人員的 Scala 指南: 類操作

梧桐雨—168發表於2008-05-19

Scala 的函式程式設計特性非常引人注目,但這並非 Java 開發人員應該對這門語言感興趣的惟一原因。實際上,Scala 融合了函式概念和麵向物件概念。為了讓 Java 和 Scala 程式設計師感到得心應手,可以瞭解一下 Scala 的物件特性,看看它們是如何在語言方面與 Java 對應的。記住,其中的一些特性並不是直接對應,或者說,在某些情況下,“對應” 更像是一種類比,而不是直接的對應。不過,遇到重要區別時,我會指出來。

Scala 和 Java 一樣使用類

我們不對 Scala 支援的類特性作冗長而抽象的討論,而是著眼於一個類的定義,這個類可用於為 Scala 平臺引入對有理數的支援(主要借鑑自 “Scala By Example”,參見 參考資料):


清單 1. rational.scala
                
class Rational(n:Int, d:Int)
{
  private def gcd(x:Int, y:Int): Int =
  {
    if (x==0) y
    else if (x<0) gcd(-x, y)
    else if (y<0) -gcd(x, -y)
    else gcd(y%x, x)
  }
  private val g = gcd(n,d)
  
  val numer:Int = n/g
  val denom:Int = d/g
  
  def +(that:Rational) =
    new Rational(numer*that.denom + that.numer*denom, denom * that.denom)
  def -(that:Rational) =
    new Rational(numer * that.denom - that.numer * denom, denom * that.denom)
  def *(that:Rational) =
    new Rational(numer * that.numer, denom * that.denom)
  def /(that:Rational) =
    new Rational(numer * that.denom, denom * that.numer)

  override def toString() =
    "Rational: [" + numer + " / " + denom + "]"
}

從詞彙上看,清單 1 的整體結構與 Java 程式碼類似,但是,這裡顯然還有一些新的元素。在詳細討論這個定義之前,先看一段使用這個新 Rational 類的程式碼:


清單 2. RunRational
                
class Rational(n:Int, d:Int)
{
  // ... as before
}

object RunRational extends Application
{
  val r1 = new Rational(1, 3)
  val r2 = new Rational(2, 5)
  val r3 = r1 - r2
  val r4 = r1 + r2
  Console.println("r1 = " + r1)
  Console.println("r2 = " + r2)
  Console.println("r3 = r1 - r2 = " + r3)
  Console.println("r4 = r1 + r2 = " + r4)
}

清單 2 中的內容平淡無奇:先建立兩個有理數,然後再建立兩個 Rational,作為前面兩個有理數的和與差,最後將這幾個數回傳到控制檯上(注意, Console.println() 來自 Scala 核心庫,位於 scala.* 中,它被隱式地匯入每個 Scala 程式中,就像 Java 程式設計中的 java.lang 一樣)。





回頁首


用多少種方法構造類?

現在,回顧一下 Rational 類定義中的第一行:


清單 3. Scala 的預設建構函式
                
class Rational(n:Int, d:Int)
{
  // ...

您也許會認為清單 3 中使用了某種類似於泛型的語法,這其實是 Rational 類的預設的、首選的建構函式:nd 是建構函式的引數。

Scala 優先使用單個建構函式,這具有一定的意義 —— 大多數類只有一個建構函式,或者通過一個建構函式將一組建構函式 “連結” 起來。如果需要,可以在一個 Rational 上定義更多的建構函式,例如:


清單 4. 建構函式鏈
                
class Rational(n:Int, d:Int)
{
  def this(d:Int) = { this(0, d) }

注意,Scala 的建構函式鏈通過呼叫首選建構函式(Int,Int 版本)實現 Java 建構函式鏈的功能。

實現細節

在處理有理數時,採取一點數值技巧將會有所幫助:也就是說,找到公分母,使某些操作變得更容易。如果要將 1/2 與 2/4 相加,那麼 Rational 類應該足夠聰明,能夠認識到 2/4 和 1/2 是相等的,並在將這兩個數相加之前進行相應的轉換。

巢狀的私有 gcd() 函式和 Rational 類中的 g 值可以實現這樣的功能。在 Scala 中呼叫建構函式時,將對整個類進行計算,這意味著將 g 初始化為 nd 的最大公分母,然後用它依次設定 nd

回顧一下 清單 1 就會發現,我建立了一個覆蓋的 toString 方法來返回 Rational 的值,在 RunRational 驅動程式程式碼中使用 toString 時,這樣做非常有用。

然而,請注意 toString 的語法:定義前面的 override 關鍵字是必需的,這樣 Scala 才能確認基類中存在相應的定義。這有助於預防因意外的輸入錯誤導致難於覺察的 bug(Java 5 中建立 @Override 註釋的動機也在於此)。還應注意,這裡沒有指定返回型別 —— 從方法體的定義很容易看出 —— 返回值沒有用 return 關鍵字顯式地標註,而在 Java 中則必須這樣做。相反,函式中的最後一個值將被隱式地當作返回值(但是,如果您更喜歡 Java 語法,也可以使用 return 關鍵字)。





回頁首


一些重要值

接下來分別是 numerdenom 的定義。這裡涉及的語法可能讓 Java 程式設計師認為 numerdenom 是公共的 Int 欄位,它們分別被初始化為 n-over-gd-over-g;但這種想法是不對的。

在形式上,Scala 呼叫無引數的 numerdenom 方法,這種方法用於建立快捷的語法以定義 accessor。Rational 類仍然有 3 個私有欄位:ndg,但是,其中的 nd 被預設定義為私有訪問,而 g 則被顯式地定義為私有訪問,它們對於外部都是隱藏的。

此時,Java 程式設計師可能會問:“nd 各自的 ‘setter’ 在哪裡?” Scala 中不存在這樣的 setter。Scala 的一個強大之處就在於,它鼓勵開發人員以預設方式建立不可改變的物件。但是,也可使用語法建立修改 Rational 內部結構的方法,但是這樣做會破壞該類固有的執行緒安全性。因此,至少對於這個例子而言,我將保持 Rational 不變。

當然還有一個問題,如何操縱 Rational 呢?與 java.lang.String 一樣,不能直接修改現有的 Rational 的值,所以惟一的辦法是根據現有類的值建立一個新的 Rational,或者從頭建立。這涉及到 4 個名稱比較古怪的方法:+-*/

與其外表相反,這並非操作符過載。





回頁首


操作符

記住,在 Scala 中一切都是物件。在上一篇 文章 中, 您看到了函式本身也是物件這一原則的應用,這使 Scala 程式設計師可以將函式賦予變數,將函式作為物件引數傳遞等等。另一個同樣重要的原則是,一切都是函式;也就是說,在此處,命名為 add 的函式與命名為 + 的函式沒有區別。在 Scala 中,所有操作符都是類的函式。只不過它們的名稱比較古怪罷了。

Rational 類中,為有理數定義了 4 種操作。它們是規範的數學操作:加、減、乘、除。每種操作以它的數學符號命名:+-*/

但是請注意,這些操作符每次操作時都構造一個新的 Rational 物件。同樣,這與 java.lang.String 非常相似,這是預設的實現,因為這樣可以產生執行緒安全的程式碼(如果執行緒沒有修改共享狀態 —— 預設情況下,跨執行緒共享的物件的內部狀態也屬於共享狀態 —— 則不會影響對那個狀態的併發訪問)。

有什麼變化?

一切都是函式,這一規則產生兩個重要影響:

首先,您已經看到,函式可以作為物件進行操縱和儲存。這使函式具有強大的可重用性,本系列 第一篇文章 對此作了探討。

第二個影響是,Scala 語言設計者提供的操作符與 Scala 程式設計師認為應該 提供的操作符之間沒有特別的差異。例如,假設提供一個 “求倒數” 操作符,這個操作符會將分子和分母調換,返回一個新的 Rational (即對於 Rational(2,5) 將返回 Rational(5,2))。如果您認為 ~ 符號最適合表示這個概念,那麼可以使用此符號作為名稱定義一個新方法,該方法將和 Java 程式碼中任何其他操作符一樣,如清單 5 所示:


清單 5. 求倒數
                
  val r6 = ~r1
  Console.println(r6) // should print [3 / 1], since r1 = [1 / 3]

在 Scala 中定義這種一元 “操作符” 需要一點技巧,但這只是語法上的問題而已:


清單 6. 如何求倒數
                
class Rational(n:Int, d:Int)
{
  // ... as before ...

  def unary_~ : Rational =
    new Rational(denom, numer)
}

當然,需要注意的地方是,必須在名稱 ~ 之前加上字首 “unary_”,告訴 Scala 編譯器它屬於一元操作符。因此,該語法將顛覆大多數物件語言中常見的傳統 reference-then-method 語法。

這條規則與 “一切都是物件” 規則結合起來,可以實現功能強大(但很簡單)的程式碼:


清單 7. 求和
                
  1 + 2 + 3 // same as 1.+(2.+(3))
  r1 + r2 + r3 // same as r1.+(r2.+(r3))

當然,對於簡單的整數加法,Scala 編譯器也會 “得到正確的結果”,它們在語法上是完全一樣的。這意味著您可以開發與 Scala 語言 “內建” 的型別完全相同的型別。

Scala 編譯器甚至會嘗試推斷具有某種預定含義的 “操作符” 的其他含義,例如 += 操作符。注意,雖然 Rational 類並沒有顯式地定義 +=,下面的程式碼仍然會正常執行:


清單 8. Scala 推斷
                  
  var r5 = new Rational(3,4)
  r5 += r1
  Console.println(r5)

列印結果時,r5 的值為 [13 / 12],結果是正確的。





回頁首


Scala 內幕

記住,Scala 將被編譯為 Java 位元組碼,這意味著它在 JVM 上執行。如果您需要證據,那麼只需注意編譯器生成以 0xCAFEBABE 開頭的 .class 檔案,就像 javac 一樣。另外請注意,如果啟動 JDK 自帶的 Java 位元組碼反編譯器(javap),並將它指向生成的 Rational 類,將會出現什麼情況,如清單 9 所示:


清單 9. 從 rational.scala 編譯的類
                  
C:\Projects\scala-classes\code>javap -private -classpath classes Rational
Compiled from "rational.scala"
public class Rational extends java.lang.Object implements scala.ScalaObject{
    private int denom;
    private int numer;
    private int g;
    public Rational(int, int);
    public Rational unary_$tilde();
    public java.lang.String toString();
    public Rational $div(Rational);
    public Rational $times(Rational);
    public Rational $minus(Rational);
    public Rational $plus(Rational);
    public int denom();
    public int numer();
    private int g();
    private int gcd(int, int);
    public Rational(int);
    public int $tag();
}


C:\Projects\scala-classes\code>

Scala 類中定義的 “操作符” 被轉換成傳統 Java 程式設計中的方法呼叫,不過它們仍使用看上去有些古怪的名稱。類中定義了兩個建構函式:一個建構函式帶有一個 int 引數,另一個帶有兩個 int 引數。您可能會注意到,大寫的 Int 型別與 java.lang.Integer 有點相似,Scala 編譯器非常聰明,會在類定義中將它們轉換成常規的 Java 原語 int

測試 Rational 類

一種著名的觀點認為,優秀的程式設計師編寫程式碼,偉大的程式設計師編寫測試;到目前為止,我還沒有對我的 Scala 程式碼嚴格地實踐這一規則,那麼現在看看將這個 Rational 類放入一個傳統的 JUnit 測試套件中會怎樣,如清單 10 所示:


清單 10. RationalTest.java
                
import org.junit.*;
import static org.junit.Assert.*;

public class RationalTest
{
    @Test public void test2ArgRationalConstructor()
    {
        Rational r = new Rational(2, 5);

        assertTrue(r.numer() == 2);
        assertTrue(r.denom() == 5);
    }
    
    @Test public void test1ArgRationalConstructor()
    {
        Rational r = new Rational(5);

        assertTrue(r.numer() == 0);
        assertTrue(r.denom() == 1);
            // 1 because of gcd() invocation during construction;
            // 0-over-5 is the same as 0-over-1
    }    
    
    @Test public void testAddRationals()
    {
        Rational r1 = new Rational(2, 5);
        Rational r2 = new Rational(1, 3);

        Rational r3 = (Rational) reflectInvoke(r1, "$plus", r2); //r1.$plus(r2);

        assertTrue(r3.numer() == 11);
        assertTrue(r3.denom() == 15);
    }
    
    // ... some details omitted
}

SUnit

現在已經有一個基於 Scala 的單元測試套件,其名稱為 SUnit。如果將 SUnit 用於清單 10 中的測試,則不需要基於 Reflection 的方法。基於 Scala 的單元測試程式碼將針對 Scala 類進行編譯,所以編譯器可以構成符號行。一些開發人員發現,使用 Scala 編寫用於測試 POJO 的單元測試實際上更加有趣。

SUnit 是標準 Scala 發行版的一部分,位於 scala.testing 包中(要了解更多關於 SUnit 的資訊,請參閱 參考資料)。

除了確認 Rational 類執行正常之外,上面的測試套件還證明可以從 Java 程式碼中呼叫 Scala 程式碼(儘管在操作符方面有點不匹配)。當然,令人高興的是,您可以將 Java 類遷移至 Scala 類,同時不必更改支援這些類的測試,然後慢慢嘗試 Scala。

您惟一可能覺得古怪的地方是操作符呼叫,在本例中就是 Rational 類中的 + 方法。回顧一下 javap 的輸出,Scala 顯然已經將 + 函式轉換為 JVM 方法 $plus,但是 Java 語言規範並不允許識別符號中出現 $ 字元(這正是它被用於巢狀和匿名巢狀類名稱中的原因)。

為了呼叫那些方法,需要用 Groovy 或 JRuby(或者其他對 $ 字元沒有限制的語言)編寫測試,或者編寫 Reflection 程式碼來呼叫它。我採用後一種方法,從 Scala 的角度看這不是那麼有趣,但是如果您有興趣的話,可以看看本文的程式碼中包含的結果(參見 下載)。

注意,只有當函式名稱不是合法的 Java 識別符號時才需要用這類方法。





回頁首


“更好的” Java

我學習 C++ 的時候,Bjarne Stroustrup 建議,學習 C++ 的一種方法是將它看作 “更好的 C 語言”(參見 參考資料)。在某些方面,如今的 Java 開發人員也可以將 Scala 看作是 “更好的 Java”,因為它提供了一種編寫傳統 Java POJO 的更簡潔的方式。考慮清單 11 中顯示的傳統 Person POJO:


清單 11. JavaPerson.java(原始 POJO)
                
public class JavaPerson
{
    public JavaPerson(String firstName, String lastName, int age)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    public String getFirstName()
    {
        return this.firstName;
    }
    public void setFirstName(String value)
    {
        this.firstName = value;
    }
    
    public String getLastName()
    {
        return this.lastName;
    }
    public void setLastName(String value)
    {
        this.lastName = value;
    }
    
    public int getAge()
    {
        return this.age;
    }
    public void setAge(int value)
    {
        this.age = value;
    }
    
    public String toString()
    {
        return "[Person: firstName" + firstName + " lastName:" + lastName +
            " age:" + age + " ]";
    }
    
    private String firstName;
    private String lastName;
    private int age;
}

現在考慮用 Scala 編寫的對等物:


清單 12. person.scala(執行緒安全的 POJO)
                
class Person(firstName:String, lastName:String, age:Int)
{
    def getFirstName = firstName
    def getLastName = lastName
    def getAge = age

    override def toString =
        "[Person firstName:" + firstName + " lastName:" + lastName +
            " age:" + age + " ]"
}

這不是一個完全匹配的替換,因為原始的 Person 包含一些可變的 setter。但是,由於原始的 Person 沒有與這些可變 setter 相關的同步程式碼,所以 Scala 版本使用起來更安全。而且,如果目標是減少 Person 中的程式碼行數,那麼可以刪除整個 getFoo 屬性方法,因為 Scala 將為每個建構函式引數生成 accessor 方法 —— firstName() 返回一個 StringlastName() 返回一個 Stringage() 返回一個 int

即使必須包含這些可變的 setter 方法,Scala 版本仍然更加簡單,如清單 13 所示:


清單 13. person.scala(完整的 POJO)
                
class Person(var firstName:String, var lastName:String, var age:Int)
{
    def getFirstName = firstName
    def getLastName = lastName
    def getAge = age
    
    def setFirstName(value:String):Unit = firstName = value
    def setLastName(value:String) = lastName = value
    def setAge(value:Int) = age = value

    override def toString =
        "[Person firstName:" + firstName + " lastName:" + lastName +
            " age:" + age + " ]"
}

注意,建構函式引數引入了 var 關鍵字。簡單來說, var 告訴編譯器這個值是可變的。因此,Scala 同時生成 accessor( String firstName(void))和 mutator(void firstName_$eq(String))方法。然後,就可以方便地建立 setFoo 屬性 mutator 方法,它在幕後使用生成的 mutator 方法。





回頁首


結束語

分享這篇文章……

digg 提交到 Digg
del.icio.us 釋出到 del.icio.us
Slashdot Slashdot 一下!

Scala 將函式概念與簡潔性相融合,同時又未失去物件的豐富特性。從本系列中您可能已經看到,Scala 還修正了 Java 語言中的一些語法問題(後見之明)。

本文是面向 Java 開發人員的 Scala 指南 系列中的第二篇文章,本文主要討論了 Scala 的物件特性,使您可以開始使用 Scala,而不必深入探究函式方面。應用目前學到的知識,您現在可以使用 Scala 減輕程式設計負擔。而且,可以使用 Scala 生成其他程式設計環境(例如 Spring 或 Hibernate )所需的 POJO。

但是,請繼續關注本系列,下期文章將開始討論 Scala 的函式方面。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/13270562/viewspace-277834/,如需轉載,請註明出處,否則將追究法律責任。

相關文章