下面是我們準備展示的主要內容:
- Scala語言的精煉,包括類型推斷的能力;
- match表達式,以及模式和case類等相關概念;
- Scala的並發,採用消息和actor機制,而不是像Java代碼那樣用老舊的鎖機制。
這些不是Scala的全部內容,只掌握它們也不可能讓你變成Scala開發高手。它們是用來吊你胃口的,只是給你幾個具體示例表明Scala可能適用於哪些場合。要走得更遠,就得做更深入的探索。你可以找些在線資源,也可以找本完整講述Scala的書,比如Joshua Suereth的Scala in Depth(Manning,2012)。
我們要解釋的第一個特性,也是Scala跟Java最重要的差別,就是它語法上的精煉性,我們就直奔主題吧。
9.1.1 簡約的Scala
Scala是採用靜態類型系統的編譯型語言。也就是說Scala代碼應該和Java代碼一樣詳細。可Scala偏偏很精煉,它太精煉了,看起來簡直和腳本語言一樣。因此Scala開發人員更加快速和高效,寫代碼的速度幾乎可以跟用動態語言編程媲美了。
我們來看一些非常簡單的代碼,瞭解一下Scala的構造方法和類。比如要寫一個簡單的現金流模型類。需要用戶提供兩項信息:現金流的額度和貨幣。用Scala應該這樣寫:
class CashFlow(amt : Double, curr : String) {
def amount = amt
def currency = curr
}
這個類只有四行(其中一行還是用來結束的右括號)。不管怎樣,它有獲取方法(但沒有設置方法)作為參數,還有一個單例構造方法。跟Java比起來,這簡直太划算了(就這麼幾行代碼)。請看相應的Java代碼:
public class CashFlow {
private final double amt;
private final String curr;
public CashFlow(double amt, String curr) {
this.amt = amt;
this.curr = curr;
}
public double getAmt {
return amt;
}
public String getCurr {
return curr;
}
}
跟Scala相比,Java代碼中的重複信息太多了,就是這種重複導致了Java代碼的冗長。
選擇Scala,讓開發人員盡量減少重複信息的輸入,IDE的界面中就可以顯示更多內容。面對稍微複雜點的邏輯時,開發人員就能見到更多代碼,因此也有望能掌握理解它所需的更多線索。
要不要省1500美元?
CashFlow
類的Scala版長度幾乎比Java版短75%。據估計,一行代碼每年的成本是32美元。如果我們假定這段代碼的生命期是5年,那在這個項目的生命期內,Scala版代碼的維護成本就會比Java代碼少花1500美元。
既然說到這兒了,我們就來看看第一個例子中展示的語法點。
類的定義(就它的參數而言)和類的構造方法是同一個東西。Scala中可以有其他的「輔助構造方法」,稍後就會談到。
類默認是公開的,所以沒必要加上
public
關鍵字。方法的返回類型是通過類型推斷確定的,但要在定義方法的
def
從句中用等號告訴編譯器做類型推斷。如果方法體只是一條語句(或表達式),那就沒必要用大括號括起來。
Scala不像Java一樣有原始類型。數字類型也是對象。
Scala的精煉不止體現在這些方面。甚至像HelloWorld這樣簡單的經典程序中都有所體現:
object HelloWorld {
def main(args : Array[String]) {
val hello = \"Hello World!\"
println(hello)
}
}
即便在這個最基本的例子中,也有幾個幫我們去除套路化代碼的特性。
關鍵字
object
告訴Scala編譯器這個類是單例類。調用
println
沒必要說明完整路徑(感謝默認引入)。沒必要在
main
方法前指明關鍵字public
和static
。不必聲明
hello
的類型,編譯器會自己找出來。不必聲明
main
的返回類型,編譯器會自動設為Unit
(等價於Java中的void
)。
這個例子中還有些相關語法需要注意一下。
跟Java和Groovy不一樣,變量的類型在變量名之後。
Scala用方括號來表示泛型,所以類型參數的表示方法是
Array[String]
,而不是String
。Array
是純正的泛型。集合類型必須指明泛型(不能像Java那樣聲明生類型1)。
分號絕對是可選的。
val
就相當於Java中的final
變量,用於聲明一個不可變變量。-
Scala應用程序的初始入口總是在
object
中。
1 生類型(raw type)是指不帶類型參數的泛型類或接口。比如泛型類Box<T>
,創建它的參數化類型時要指明類型參數的真實類型:Box<Integer> intBox = new Box<>;
。如果忽略了類型參數,Box rawBox = new Box;
則是創建了一個生類型。——譯者注
在後續幾節中,我們會詳細解釋這些語法是如何工作的,並且我們還會再選幾個讓你更省手指頭的Scala創新介紹一下。我們也會討論Scala的函數式編程,它對於編寫精煉的代碼非常有幫助。現在,我們先來討論一個強大的Scala「本地」特性。
9.1.2 match表達式
Scala有一種非常強大的結構:match
表達式。最簡單的match
用法跟Java的switch
差不多,但match
的表達力要強得多。match
表達式的形式取決於case
從句中的表達式結構。Scala調用不同類型的case
從句模式,但要注意,這些所謂的模式跟正則表達式裡的「模式」是截然不同的(儘管在match
表達式裡也可以用正則表達式模式)。
先看一個熟悉的例子。1.3.1節那個帶字符串的swtich
被翻譯成了Scala代碼,請看:
var frenchDayOfWeek = args(0) match {
case \"Sunday\" => \"Dimanche\"
case \"Monday\" => \"Lundi\"
case \"Tuesday\" => \"Mardi\"
case \"Wednesday\" => \"Mercredi\"
case \"Thursday\" => \"Jeudi\"
case \"Friday\" => \"Vendredi\"
case \"Saturday\" => \"Samedi\"
case _ => \"Error: \'\"+ args(0) +\"\' is not a day of the week\"
}
println(frenchDayOfWeek)
我們在這個例子中只用到了兩種最基本的模式:用來確定是周幾的常量模式和處理默認情況的_
模式,後面我們還會遇到其他模式。
從語言的純粹性來看,可以說Scala的語法比Java更清晰,也更正規,至少從下面這兩點來看是這樣的:
- 默認
case
不需要另外一個不同的關鍵字; - 單個
case
不會像Java中那樣進入下一個case
,所以也不需要break
。
這個例子中的其他語法點如下所示。
關鍵字
var
用來聲明一個可變(非final
)變量。沒有必要盡量不要用它,但有時候確實需要它。數組用圓括號訪問,比如
args(0)
是指main
的第一個參數。總應該包括默認
case
。如果Scala在運行時在所有case
中都找不到匹配項,就會拋出MatchError
。這絕不是你想看到的。Scala支持間接方法調用,所以可以把
args(0).match({ ... })
寫成args(0) match { ... }
。
到目前為止一切都好。match
看起來就像稍微簡潔些的switch
。但這只是它眾多模式中最像Java的。Scala中有大量使用不同模式的語言結構。比如說,有一種類型化模式,對於處理類型不確定的數據很有用,不用像Java那樣弄一堆亂糟糟的類型轉換或instanceof
測試:
def storageSize(obj: Any) = obj match {
case s: String => s.length
case i: Int => 4
case _ => -1
}
這個極其簡單的方法以一個Any
類型(即未知類型)的值為參數,然後用模式分別處理String
和Int
類型的值。每個case
都給要處理的值綁定了一個臨時別名,以便必要時可以調用其中的方法。
在Scala的異常處理代碼中有一個跟變量模式非常相似的語法形式。下面是一段改編自第11章ScalaTest框架的類加載代碼:
def getReporter(repClassName: String, loader: ClassLoader): Reporter = {
try {
val reporterCl: java.lang.Class[_] = loader.loadClass(repClassName)
reporterCl.newInstance.asInstanceOf[Reporter]
}
catch {
case e: ClassNotFoundException => {
val msg = \"Can\'t load reporter class\"
val iae = new IllegalArgumentException(msg)
iae.initCause(e)
throw iae
}
case e: InstantiationException => {
val msg = \"Can\'t instantiate Reporter\"
val iae = new IllegalArgumentException(msg)
iae.initCause(e)
throw iae
}
...
}
}
在getReporter
中,要加載一個定制的report
類(通過反射),以便在運行測試集時輸出報告。在類加載和實例化過程中很多事都可能出錯,所以要有個try-catch
塊來保護程序執行。
catch
塊起到的作用就跟在異常類型上放match
表達式類似。 case
類的這種思路還可以進一步延伸,接下來我們就來討論這個。
9.1.3 case類
match
表達式的最強用法之一就是跟case
類(可以看成是枚舉概念面向對象的擴展)相結合。我們來看一個溫度過高發出報警信號的例子:
case class TemperatureAlarm(temp : Double)
單這一行代碼就可以定義一個絕對有效的case
類。在Java中相應的類大概應該是這樣子:
public class TemperatureAlarm {
private final double temp;
public TemperatureAlarm(double temp) {
this.temp = temp;
}
public double getTemp {
return temp;
}
@Override
public String toString {
return \"TemperatureAlarm [temp=\" + temp + \"]\";
}
@Override
public int hashCode {
final int prime = 31;
int result = 1;
long temp;
temp = Double.doubleToLongBits(this.temp);
result = prime * result + (int) (temp ^ (temp >>>32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass != obj.getClass)
return false;
TemperatureAlarm other = (TemperatureAlarm) obj;
if (Double.doubleToLongBits(temp) !=
Double.doubleToLongBits(other.temp))
return false;
return true;
}
}
只需加個case
關鍵字就可以讓Scala編譯器生成這些額外的方法。它還會生成很多額外的架子方法。大多數情況下,開發人員都不會直接使用這些方法。它們是為某些Scala特性提供運行時支持的——能以「自然的Scala」方式使用case
類。
創建case
類實例不需要關鍵字new
,像這樣:
val alarm = TemperatureAlarm(99.9)
這進一步強化了case
類是類似於「參數化枚舉類型」或某種形式的值類型的觀點。
Scala中的相等
Scala認為Java用
==
表示「引用相等」是個錯誤。所以在Scala中,==
和.equals
是一樣的。如果需要判斷引用相等,可以用===
。case
類的.equals
方法只有在兩個實例的所有參數值都一樣時才會返回true
。
case
類跟構造器模式非常合,請看:
def ctorMatchExample(sthg : AnyRef) = {
val msg = sthg match {
case Heartbeat => 0
case TemperatureAlarm(temp) => \"Tripped at temp \"+ temp
case _ => \"No match\"
}
println(msg)
}
我們去看看Scala觀光之旅的最後一站:基於actor的並髮結構。
9.1.4 actor
Scala選擇用actor機制來實現並發編程。它們提供了一個異步並發模型,通過在代碼單元間傳遞消息實現並發。很多開發人員都發現這種並發模型比Java提供的基於鎖機制、默認共享的並發模型易用(不過Scala的底層模型也是JMM)。
來看個例子。假設我們在第4章遇到的獸醫需要監控診所裡動物的健康狀況(尤其是體溫)。按我們的想法,溫度感應器應該會將它們的讀數消息發送給中心監控軟件。
在Scala中,我們可以用一個actor類TemperatureMonitor
對這種設置建模。應該有兩種不同的消息:一種是標準的「心跳」消息,一種是TemperatureAlarm
消息。第二種消息會帶一個參數,表明那個警報器的溫度超出了限值。代碼清單9-1中列出了這些類的代碼。
代碼清單9-1 與actor的簡單通信
case object Heartbeat
case class TemperatureAlarm(temp : Double)
import scala.actors._
class TemperatureMonitor extends Actor {
var tripped : Boolean = false
var tripTemp : Double = 0.0
def act = { //重寫actor中的act方法
while (true) {
receive { //接受新消息
case Heartbeat => 0
case TemperatureAlarm(temp) =>
tripped = true
tripTemp = temp
case _ => println(\"No match\")
}
}
}
}
監控actor會對三種不同的case
做出響應(通過receive
)。第一個是心跳消息,告訴你一切正常。因為這個case
類沒有參數,所以技術上來說它是一個單例實例,可以按case
對像引用。actor
在收到心跳消息時什麼也不用做。
如果收到TemperatureAlarm
消息,actor
會保存警報器上的溫度值。你應該想像得出,獸醫有另外的代碼定期檢查TemperatureMonitor
actor,看有沒有警報被觸發。
最後還有個default case
。這是為了確保有任何不期而至的消息溜進actor環境時能被捕獲到。如果沒有這個一切全包的 case
,actor如果看到不認識的消息類型就會拋出異常。我們在本章的最後還會再次討論actor的更多細節,但Scala的並發是個非常大的主題,而且在這本書裡我們也不想讓你淺嘗輒止。
我們快速瀏覽了Scala的一些亮點。希望其中的某些特性已經燃起了你的興趣之火。在下一節,我們會花點時間聊聊你可能會(也可能不會)在自己的項目中選擇使用Scala的原因。