讀古今文學網 > Java程序員修煉之道 > 9.1 走馬觀花Scala >

9.1 走馬觀花Scala

下面是我們準備展示的主要內容:

  • 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方法前指明關鍵字publicstatic

  • 不必聲明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類型(即未知類型)的值為參數,然後用模式分別處理StringInt類型的值。每個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的原因。