Java™ 開發人員可以將對象作為理解 Scala 的出發點。本文是面向 Java 開發人員的 Scala 指南 系列 的第二期,作者 Ted Neward 遵循對一種語言進行評價的基本前提:一種語言的威力可以直接通過它集成新功能的能力衡量,在本文中就是指對復數的支持。跟隨本文,您將了解在 Scala 中與類的定義和使用有關的一些有趣特性。
在上一期 文章 中,您只是稍微了解了一些 Scala 語法,這些是運行 Scala 程序和了解其簡單特性的最基本要求。通過上一篇文章中的 Hello World 和 Timer 示例程序,您了解了 Scala 的 Application 類、方法定義和匿名函數的語法,還稍微了解了 Array[] 和一些類型推斷方面的知識。Scala 還提供了很多其他特性,本文將研究 Scala 編程中的一些較復雜方面。
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 類的默認的、首選的構造函數:n 和 d 是構造函數的參數。
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 初始化為 n 和 d 的最大公分母,然後用它依次設置 n 和 d。
回顧一下 清單 1 就會發現,我創建了一個覆蓋的 toString 方法來返回 Rational 的值,在 RunRational 驅動程序代碼中使用 toString 時,這樣做非常有用。
然而,請注意 toString 的語法:定義前面的 override 關鍵字是必需的,這樣 Scala 才能確認基類中存在相應的定義。這有助於預防因意外的輸入錯誤導致難於覺察的 bug(Java 5 中創建 @Override 注釋的動機也在於此)。還應注意,這裡沒有指定返回類型 —— 從方法體的定義很容易看出 —— 返回值沒有用 return 關鍵字顯式地標注,而在 Java 中則必須這樣做。相反,函數中的最後一個值將被隱式地當作返回值(但是,如果您更喜歡 Java 語法,也可以使用 return 關鍵字)。
一些重要值
接下來分別是 numer 和 denom 的定義。這裡涉及的語法可能讓 Java 程序員認為 numer 和 denom 是公共的 Int 字段,它們分別被初始化為 n-over-g 和 d-over-g;但這種想法是不對的。
在形式上,Scala 調用無參數的 numer 和 denom 方法,這種方法用於創建快捷的語法以定義 accessor。Rational 類仍然有 3 個私有字段:n、d 和 g,但是,其中的 n 和 d 被默認定義為私有訪問,而 g 則被顯式地定義為私有訪問,它們對於外部都是隱藏的。
此時,Java 程序員可能會問:“n 和 d 各自的 ‘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
}
除了確認 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() 返回一個 String,lastName() 返回一個 String,age() 返回一個 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 方法。
結束語
Scala 將函數概念與簡潔性相融合,同時又未失去對象的豐富特性。從本系列中您可能已經看到,Scala 還修正了 Java 語言中的一些語法問題(後見之明)。
本文是面向 Java 開發人員的 Scala 指南 系列中的第二篇文章,本文主要討論了 Scala 的對象特性,使您可以開始使用 Scala,而不必深入探究函數方面。應用目前學到的知識,您現在可以使用 Scala 減輕編程負擔。而且,可以使用 Scala 生成其他編程環境(例如 Spring 或 Hibernate )所需的 POJO。
但是,請繼續關注本系列,下期文章將開始討論 Scala 的函數方面。
原文地址:
http://www.ibm.com/developerworks/cn/java/j-scala02198.html