從Java走進(jìn)Scala:理解Scala的類(lèi)語(yǔ)法和語(yǔ)義
Scala 的函數(shù)編程特性非常引人注目,但這并非 Java 開(kāi)發(fā)人員應(yīng)該對(duì)這門(mén)語(yǔ)言感興趣的惟一原因。實(shí)際上,Scala 融合了函數(shù)概念和面向?qū)ο蟾拍?。為了?Java 和 Scala 程序員感到得心應(yīng)手,可以了解一下 Scala 的對(duì)象特性,看看它們是如何在語(yǔ)言方面與 Java 對(duì)應(yīng)的。記住,其中的一些特性并不是直接對(duì)應(yīng),或者說(shuō),在某些情況下,“對(duì)應(yīng)” 更像是一種類(lèi)比,而不是直接的對(duì)應(yīng)。不過(guò),遇到重要區(qū)別時(shí),我會(huì)指出來(lái)。
Scala 和 Java 一樣使用類(lèi)
我們不對(duì) Scala 支持的類(lèi)特性作冗長(zhǎng)而抽象的討論,而是著眼于一個(gè)類(lèi)的定義,這個(gè)類(lèi)可用于為 Scala 平臺(tái)引入對(duì)有理數(shù)的支持:
清單 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 的整體結(jié)構(gòu)與 Java 代碼類(lèi)似,但是,這里顯然還有一些新的元素。在詳細(xì)討論這個(gè)定義之前,先看一段使用這個(gè)新 Rational 類(lèi)的代碼:
清單 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 中的內(nèi)容平淡無(wú)奇:先創(chuàng)建兩個(gè)有理數(shù),然后再創(chuàng)建兩個(gè) Rational,作為前面兩個(gè)有理數(shù)的和與差,最后將這幾個(gè)數(shù)回傳到控制臺(tái)上(注意, Console.println() 來(lái)自 Scala 核心庫(kù),位于 scala.* 中,它被隱式地導(dǎo)入每個(gè) Scala 程序中,就像 Java 編程中的 java.lang 一樣)。
用多少種方法構(gòu)造類(lèi)?
現(xiàn)在,回顧一下 Rational 類(lèi)定義中的第一行:
清單 3. Scala 的默認(rèn)構(gòu)造函數(shù)
- class Rational(n:Int, d:Int)
- {
- // ...
您也許會(huì)認(rèn)為清單 3 中使用了某種類(lèi)似于泛型的語(yǔ)法,這其實(shí)是 Rational 類(lèi)的默認(rèn)的、首選的構(gòu)造函數(shù):n 和 d 是構(gòu)造函數(shù)的參數(shù)。
Scala 優(yōu)先使用單個(gè)構(gòu)造函數(shù),這具有一定的意義 —— 大多數(shù)類(lèi)只有一個(gè)構(gòu)造函數(shù),或者通過(guò)一個(gè)構(gòu)造函數(shù)將一組構(gòu)造函數(shù) “鏈接” 起來(lái)。如果需要,可以在一個(gè) Rational 上定義更多的構(gòu)造函數(shù),例如:
清單 4. 構(gòu)造函數(shù)鏈
- class Rational(n:Int, d:Int)
- {
- def this(d:Int) = { this(0, d) }
注意,Scala 的構(gòu)造函數(shù)鏈通過(guò)調(diào)用首選構(gòu)造函數(shù)(Int,Int 版本)實(shí)現(xiàn) Java 構(gòu)造函數(shù)鏈的功能。
#p#
實(shí)現(xiàn)細(xì)節(jié)
在處理有理數(shù)時(shí),采取一點(diǎn)數(shù)值技巧將會(huì)有所幫助:也就是說(shuō),找到公分母,使某些操作變得更容易。如果要將 1/2 與 2/4 相加,那么 Rational 類(lèi)應(yīng)該足夠聰明,能夠認(rèn)識(shí)到 2/4 和 1/2 是相等的,并在將這兩個(gè)數(shù)相加之前進(jìn)行相應(yīng)的轉(zhuǎn)換。
嵌套的私有 gcd() 函數(shù)和 Rational 類(lèi)中的 g 值可以實(shí)現(xiàn)這樣的功能。在 Scala 中調(diào)用構(gòu)造函數(shù)時(shí),將對(duì)整個(gè)類(lèi)進(jìn)行計(jì)算,這意味著將 g 初始化為 n 和 d 的最大公分母,然后用它依次設(shè)置 n 和 d。
回顧一下 清單 1 就會(huì)發(fā)現(xiàn),我創(chuàng)建了一個(gè)覆蓋的 toString 方法來(lái)返回 Rational 的值,在 RunRational 驅(qū)動(dòng)程序代碼中使用 toString 時(shí),這樣做非常有用。
然而,請(qǐng)注意 toString 的語(yǔ)法:定義前面的 override 關(guān)鍵字是必需的,這樣 Scala 才能確認(rèn)基類(lèi)中存在相應(yīng)的定義。這有助于預(yù)防因意外的輸入錯(cuò)誤導(dǎo)致難于覺(jué)察的 bug(Java 5 中創(chuàng)建 @Override 注釋的動(dòng)機(jī)也在于此)。還應(yīng)注意,這里沒(méi)有指定返回類(lèi)型 —— 從方法體的定義很容易看出 —— 返回值沒(méi)有用 return 關(guān)鍵字顯式地標(biāo)注,而在 Java 中則必須這樣做。相反,函數(shù)中的最后一個(gè)值將被隱式地當(dāng)作返回值(但是,如果您更喜歡 Java 語(yǔ)法,也可以使用 return 關(guān)鍵字)。
一些重要值
接下來(lái)分別是 numer 和 denom 的定義。這里涉及的語(yǔ)法可能讓 Java 程序員認(rèn)為 numer 和 denom 是公共的 Int 字段,它們分別被初始化為 n-over-g 和 d-over-g;但這種想法是不對(duì)的。
在形式上,Scala 調(diào)用無(wú)參數(shù)的 numer 和 denom 方法,這種方法用于創(chuàng)建快捷的語(yǔ)法以定義 accessor。Rational 類(lèi)仍然有 3 個(gè)私有字段:n、d 和 g,但是,其中的 n 和 d 被默認(rèn)定義為私有訪問(wèn),而 g 則被顯式地定義為私有訪問(wèn),它們對(duì)于外部都是隱藏的。
此時(shí),Java 程序員可能會(huì)問(wèn):“n 和 d 各自的 ‘setter’ 在哪里?” Scala 中不存在這樣的 setter。Scala 的一個(gè)強(qiáng)大之處就在于,它鼓勵(lì)開(kāi)發(fā)人員以默認(rèn)方式創(chuàng)建不可改變的對(duì)象。但是,也可使用語(yǔ)法創(chuàng)建修改 Rational 內(nèi)部結(jié)構(gòu)的方法,但是這樣做會(huì)破壞該類(lèi)固有的線程安全性。因此,至少對(duì)于這個(gè)例子而言,我將保持 Rational 不變。
當(dāng)然還有一個(gè)問(wèn)題,如何操縱 Rational 呢?與 java.lang.String 一樣,不能直接修改現(xiàn)有的 Rational 的值,所以惟一的辦法是根據(jù)現(xiàn)有類(lèi)的值創(chuàng)建一個(gè)新的 Rational,或者從頭創(chuàng)建。這涉及到 4 個(gè)名稱(chēng)比較古怪的方法:+、 -、* 和 /。
與其外表相反,這并非操作符重載。
操作符
記住,在 Scala 中一切都是對(duì)象。在上一篇 文章 中, 您看到了函數(shù)本身也是對(duì)象這一原則的應(yīng)用,這使 Scala 程序員可以將函數(shù)賦予變量,將函數(shù)作為對(duì)象參數(shù)傳遞等等。另一個(gè)同樣重要的原則是,一切都是函數(shù);也就是說(shuō),在此處,命名為 add 的函數(shù)與命名為 + 的函數(shù)沒(méi)有區(qū)別。在 Scala 中,所有操作符都是類(lèi)的函數(shù)。只不過(guò)它們的名稱(chēng)比較古怪罷了。
在 Rational 類(lèi)中,為有理數(shù)定義了 4 種操作。它們是規(guī)范的數(shù)學(xué)操作:加、減、乘、除。每種操作以它的數(shù)學(xué)符號(hào)命名:+、-、 * 和 /。
但是請(qǐng)注意,這些操作符每次操作時(shí)都構(gòu)造一個(gè)新的 Rational 對(duì)象。同樣,這與 java.lang.String 非常相似,這是默認(rèn)的實(shí)現(xiàn),因?yàn)檫@樣可以產(chǎn)生線程安全的代碼(如果線程沒(méi)有修改共享狀態(tài) —— 默認(rèn)情況下,跨線程共享的對(duì)象的內(nèi)部狀態(tài)也屬于共享狀態(tài) —— 則不會(huì)影響對(duì)那個(gè)狀態(tài)的并發(fā)訪問(wèn))。
有什么變化?
一切都是函數(shù),這一規(guī)則產(chǎn)生兩個(gè)重要影響:
首先,您已經(jīng)看到,函數(shù)可以作為對(duì)象進(jìn)行操縱和存儲(chǔ)。這使函數(shù)具有強(qiáng)大的可重用性,本系列 第一篇文章 對(duì)此作了探討。
第二個(gè)影響是,Scala 語(yǔ)言設(shè)計(jì)者提供的操作符與 Scala 程序員認(rèn)為應(yīng)該 提供的操作符之間沒(méi)有特別的差異。例如,假設(shè)提供一個(gè) “求倒數(shù)” 操作符,這個(gè)操作符會(huì)將分子和分母調(diào)換,返回一個(gè)新的 Rational (即對(duì)于 Rational(2,5) 將返回 Rational(5,2))。如果您認(rèn)為 ~ 符號(hào)最適合表示這個(gè)概念,那么可以使用此符號(hào)作為名稱(chēng)定義一個(gè)新方法,該方法將和 Java 代碼中任何其他操作符一樣,如清單 5 所示:
清單 5. 求倒數(shù)
- val r6 = ~r1
- Console.println(r6) // should print [3 / 1], since r1 = [1 / 3]
在 Scala 中定義這種一元 “操作符” 需要一點(diǎn)技巧,但這只是語(yǔ)法上的問(wèn)題而已:
清單 6. 如何求倒數(shù)
- class Rational(n:Int, d:Int)
- {
- // ... as before ...
- def unary_~ : Rational =
- new Rational(denom, numer)
- }
當(dāng)然,需要注意的地方是,必須在名稱(chēng) ~ 之前加上前綴 “unary_”,告訴 Scala 編譯器它屬于一元操作符。因此,該語(yǔ)法將顛覆大多數(shù)對(duì)象語(yǔ)言中常見(jiàn)的傳統(tǒng) reference-then-method 語(yǔ)法。
這條規(guī)則與 “一切都是對(duì)象” 規(guī)則結(jié)合起來(lái),可以實(shí)現(xiàn)功能強(qiáng)大(但很簡(jiǎn)單)的代碼:
清單 7. 求和
- 1 + 2 + 3 // same as 1.+(2.+(3))
- r1 + r2 + r3 // same as r1.+(r2.+(r3))
當(dāng)然,對(duì)于簡(jiǎn)單的整數(shù)加法,Scala 編譯器也會(huì) “得到正確的結(jié)果”,它們?cè)谡Z(yǔ)法上是完全一樣的。這意味著您可以開(kāi)發(fā)與 Scala 語(yǔ)言 “內(nèi)置” 的類(lèi)型完全相同的類(lèi)型。
Scala 編譯器甚至?xí)L試推斷具有某種預(yù)定含義的 “操作符” 的其他含義,例如 += 操作符。注意,雖然 Rational 類(lèi)并沒(méi)有顯式地定義 +=,下面的代碼仍然會(huì)正常運(yùn)行:
清單 8. Scala 推斷
- var r5 = new Rational(3,4)
- r5 += r1
- Console.println(r5)
打印結(jié)果時(shí),r5 的值為 [13 / 12],結(jié)果是正確的。
#p#
Scala 內(nèi)幕
記住,Scala 將被編譯為 Java 字節(jié)碼,這意味著它在 JVM 上運(yùn)行。如果您需要證據(jù),那么只需注意編譯器生成以 0xCAFEBABE 開(kāi)頭的 .class 文件,就像 javac 一樣。另外請(qǐng)注意,如果啟動(dòng) JDK 自帶的 Java 字節(jié)碼反編譯器(javap),并將它指向生成的 Rational 類(lèi),將會(huì)出現(xiàn)什么情況,如清單 9 所示:
清單 9. 從 rational.scala 編譯的類(lèi)
- 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 類(lèi)中定義的 “操作符” 被轉(zhuǎn)換成傳統(tǒng) Java 編程中的方法調(diào)用,不過(guò)它們?nèi)允褂每瓷先ビ行┕殴值拿Q(chēng)。類(lèi)中定義了兩個(gè)構(gòu)造函數(shù):一個(gè)構(gòu)造函數(shù)帶有一個(gè) int 參數(shù),另一個(gè)帶有兩個(gè) int 參數(shù)。您可能會(huì)注意到,大寫(xiě)的 Int 類(lèi)型與 java.lang.Integer 有點(diǎn)相似,Scala 編譯器非常聰明,會(huì)在類(lèi)定義中將它們轉(zhuǎn)換成常規(guī)的 Java 原語(yǔ) int。
測(cè)試 Rational 類(lèi)
一種著名的觀點(diǎn)認(rèn)為,優(yōu)秀的程序員編寫(xiě)代碼,偉大的程序員編寫(xiě)測(cè)試;到目前為止,我還沒(méi)有對(duì)我的 Scala 代碼嚴(yán)格地實(shí)踐這一規(guī)則,那么現(xiàn)在看看將這個(gè) Rational 類(lèi)放入一個(gè)傳統(tǒng)的 JUnit 測(cè)試套件中會(huì)怎樣,如清單 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
現(xiàn)在已經(jīng)有一個(gè)基于 Scala 的單元測(cè)試套件,其名稱(chēng)為 SUnit。如果將 SUnit 用于清單 10 中的測(cè)試,則不需要基于 Reflection 的方法?;?Scala 的單元測(cè)試代碼將針對(duì) Scala 類(lèi)進(jìn)行編譯,所以編譯器可以構(gòu)成符號(hào)行。一些開(kāi)發(fā)人員發(fā)現(xiàn),使用 Scala 編寫(xiě)用于測(cè)試 POJO 的單元測(cè)試實(shí)際上更加有趣。
SUnit 是標(biāo)準(zhǔn) Scala 發(fā)行版的一部分,位于 scala.testing 包中(要了解更多關(guān)于 SUnit 的信息,請(qǐng)參閱 參考資料)。
除了確認(rèn) Rational 類(lèi)運(yùn)行正常之外,上面的測(cè)試套件還證明可以從 Java 代碼中調(diào)用 Scala 代碼(盡管在操作符方面有點(diǎn)不匹配)。當(dāng)然,令人高興的是,您可以將 Java 類(lèi)遷移至 Scala 類(lèi),同時(shí)不必更改支持這些類(lèi)的測(cè)試,然后慢慢嘗試 Scala。
您惟一可能覺(jué)得古怪的地方是操作符調(diào)用,在本例中就是 Rational 類(lèi)中的 + 方法?;仡櫼幌?javap 的輸出,Scala 顯然已經(jīng)將 + 函數(shù)轉(zhuǎn)換為 JVM 方法 $plus,但是 Java 語(yǔ)言規(guī)范并不允許標(biāo)識(shí)符中出現(xiàn) $ 字符(這正是它被用于嵌套和匿名嵌套類(lèi)名稱(chēng)中的原因)。
為了調(diào)用那些方法,需要用 Groovy 或 JRuby(或者其他對(duì) $ 字符沒(méi)有限制的語(yǔ)言)編寫(xiě)測(cè)試,或者編寫(xiě) Reflection 代碼來(lái)調(diào)用它。我采用后一種方法,從 Scala 的角度看這不是那么有趣,但是如果您有興趣的話,可以看看本文的代碼中包含的結(jié)果(參見(jiàn) 下載)。
注意,只有當(dāng)函數(shù)名稱(chēng)不是合法的 Java 標(biāo)識(shí)符時(shí)才需要用這類(lèi)方法。
#p#
“更好的” Java
我學(xué)習(xí) C++ 的時(shí)候,Bjarne Stroustrup 建議,學(xué)習(xí) C++ 的一種方法是將它看作 “更好的 C 語(yǔ)言”(參見(jiàn) 參考資料)。在某些方面,如今的 Java 開(kāi)發(fā)人員也可以將 Scala 看作是 “更好的 Java”,因?yàn)樗峁┝艘环N編寫(xiě)傳統(tǒng) Java POJO 的更簡(jiǎn)潔的方式。考慮清單 11 中顯示的傳統(tǒng) 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;
- }
現(xiàn)在考慮用 Scala 編寫(xiě)的對(duì)等物:
清單 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 + " ]"
- }
這不是一個(gè)完全匹配的替換,因?yàn)樵嫉?Person 包含一些可變的 setter。但是,由于原始的 Person 沒(méi)有與這些可變 setter 相關(guān)的同步代碼,所以 Scala 版本使用起來(lái)更安全。而且,如果目標(biāo)是減少 Person 中的代碼行數(shù),那么可以刪除整個(gè) getFoo 屬性方法,因?yàn)?Scala 將為每個(gè)構(gòu)造函數(shù)參數(shù)生成 accessor 方法 —— firstName() 返回一個(gè) String,lastName() 返回一個(gè) String,age() 返回一個(gè) int。
即使必須包含這些可變的 setter 方法,Scala 版本仍然更加簡(jiǎn)單,如清單 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 + " ]"
- }
注意,構(gòu)造函數(shù)參數(shù)引入了 var 關(guān)鍵字。簡(jiǎn)單來(lái)說(shuō), var 告訴編譯器這個(gè)值是可變的。因此,Scala 同時(shí)生成 accessor( String firstName(void))和 mutator(void firstName_$eq(String))方法。然后,就可以方便地創(chuàng)建 setFoo 屬性 mutator 方法,它在幕后使用生成的 mutator 方法。
結(jié)束語(yǔ)
Scala 將函數(shù)概念與簡(jiǎn)潔性相融合,同時(shí)又未失去對(duì)象的豐富特性。從本系列中您可能已經(jīng)看到,Scala 還修正了 Java 語(yǔ)言中的一些語(yǔ)法問(wèn)題(后見(jiàn)之明)。
本文是面向 Java 開(kāi)發(fā)人員的 Scala 指南 系列中的第二篇文章,本文主要討論了 Scala 的對(duì)象特性,使您可以開(kāi)始使用 Scala,而不必深入探究函數(shù)方面。應(yīng)用目前學(xué)到的知識(shí),您現(xiàn)在可以使用 Scala 減輕編程負(fù)擔(dān)。而且,可以使用 Scala 生成其他編程環(huán)境(例如 Spring 或 Hibernate )所需的 POJO。
【相關(guān)閱讀】



















