JAVA程式設計師的SCALA教程
作者:Michel Schinz和Philipp Haller
介紹
本文件簡要介紹了Scala語言和編譯器。它適用於已經擁有一些程式設計經驗並希望瞭解他們可以使用Scala做什麼的人。假定了面向物件程式設計的基本知識,特別是在Java中。
第一個例子
作為第一個例子,我們將使用標準的Hello world程式。它不是很吸引人,但可以很容易地演示Scala工具的使用,而不必過多地瞭解語言。以下是它的外觀:
object HelloWorld {
def main(args: Array[String]) {
println("Hello, world!")
}
}
Java程式設計師應該熟悉這個程式的結構:它包含一個呼叫main
println
,其中友好問候語作為引數。該main
方法不返回值(它是一個過程方法)。因此,沒有必要宣告返回型別。
Java程式設計師不太熟悉的是object
包含該main
方法的宣告。這樣的宣告引入了通常所說的單例物件,即具有單個例項的類。因此,上面的宣告聲明瞭一個被呼叫的類HelloWorld
和該類的一個例項,也稱為HelloWorld
。該例項是在第一次使用時按需建立的。
精明的讀者可能已經注意到該main
方法未在static
此處宣告。這是因為Scala中不存在靜態成員(方法或欄位)。Scala程式設計師不是定義靜態成員,而是在單例物件中宣告這些成員。
編譯示例
為了編譯示例,我們使用scalac
Scala編譯器。scalac
像大多數編譯器一樣工作:它將原始檔作為引數,可能是一些選項,並生成一個或多個目標檔案。它生成的目標檔案是標準的Java類檔案。
如果我們將上述程式儲存在一個名為的檔案中 HelloWorld.scala
,我們可以通過發出以下命令來編譯它(大於號>
表示shell提示符,不應該鍵入):
> scalac HelloWorld.scala
這將在當前目錄中生成一些類檔案。其中一個將被呼叫HelloWorld.class
,幷包含一個可以使用該scala
命令直接執行的類,如下節所示。
執行示例
編譯完成後,可以使用該scala
java
用於執行Java程式的命令非常相似,並且接受相同的選項。上面的例子可以使用以下命令執行,該命令產生預期的輸出:
> scala -classpath . HelloWorld
Hello, world!
與Java互動
Scala的優勢之一是它可以很容易地與Java程式碼進行互動。java.lang
預設情況下會匯入包中的所有類,而其他類需要顯式匯入。
讓我們看一個證明這一點的例子。我們希望根據特定國家/地區使用的慣例獲取並格式化當前日期,例如法國。(瑞士法語區等其他地區使用相同的慣例。)
Java的類庫定義了強大的實用程式類,例如 Date
和DateFormat
。由於Scala與Java無縫地互操作,因此不需要在Scala類庫中實現等效類 - 我們可以簡單地匯入相應Java包的類:
import java.util.{Date, Locale}
import java.text.DateFormat
import java.text.DateFormat._
object FrenchDate {
def main(args: Array[String]) {
val now = new Date
val df = getDateInstance(LONG, Locale.FRANCE)
println(df format now)
}
}
Scala的import語句看起來與Java相當,但它更強大。可以從同一個包中匯入多個類,方法是將它們用大括號括起來,就像在第一行一樣。另一個區別是,在匯入包或類的所有名稱時,使用下劃線字元(_
)而不是星號(*
)。這是因為星號是有效的Scala識別符號(例如方法名稱),我們稍後會看到。
因此,第三行上的import語句將匯入DateFormat
該類的所有成員。這使靜態方法 getDateInstance
和靜態欄位LONG
直接可見。
在main
方法內部,我們首先建立一個Java Date
類的例項, 預設情況下包含當前日期。接下來,我們使用getDateInstance
之前匯入的靜態方法定義日期格式。最後,我們列印根據本地化DateFormat
例項格式化的當前日期。最後一行顯示了Scala語法的一個有趣屬性。採用一個引數的方法可以與中綴語法一起使用。也就是說,表達
df format now
寫作表示式只是另一種略顯冗長的方式
df.format(now)
這似乎是一個較小的句法細節,但它有重要的後果,其中一個將在下一節中探討。
在結束本節關於與Java整合的部分時,應該注意的是,也可以從Java類繼承並直接在Scala中實現Java介面。
一切都是物件
Scala是一種純粹的面嚮物件語言,因為 一切都是物件,包括數字或函式。它在這方面與Java不同,因為Java將原始型別(例如boolean
和int
)與引用型別區分開來,並且不允許將函式作為值來操作。
數字是物件
由於數字是物件,因此它們也有方法。事實上,算術表示式如下:
1 + 2 * 3 / x
由方法呼叫組成,因為它等同於下面的表示式,正如我們在上一節中看到的那樣:
(1).+(((2).*(3))./(x))
這也意味著+
,*
等在斯卡拉有效的識別符號。
第二個版本中數字的括號是必要的,因為Scala的詞法分析器使用最長的匹配規則作為標記。因此,它會破壞以下表達式:
1.+(2)
入令牌1.
,+
和2
。選擇此標記化的原因是因為1.
比較長的有效匹配1
。令牌1.
被解釋為文字1.0
,使其成為一個Double
而不是一個Int
。將表示式寫為:
1.+(2)
防止1
被解釋為Double
。
功能是物件
也許Java程式設計師更令人驚訝,函式也是Scala中的物件。因此,可以將函式作為引數傳遞,將它們儲存在變數中,並從其他函式返回它們。這種將函式作為值進行操作的能力是一種非常有趣的程式設計正規化(稱為函數語言程式設計)的基石之一 。
作為將函式用作值的有用原因的一個非常簡單的例子,讓我們考慮一個計時器函式,其目的是每秒執行一些操作。我們如何將動作傳遞給它?在邏輯上,作為一種功能。這種非常簡單的函式傳遞應該為許多程式設計師所熟悉:它通常用在使用者介面程式碼中,用於註冊在某些事件發生時呼叫的回撥函式。
在下面的程式中,呼叫timer函式 oncePerSecond
,並獲得一個回撥函式作為引數。這個函式的型別是寫的() => Unit
,是所有函式的型別,它們不帶引數並且什麼都不返回(型別 Unit
與void
C / C ++ 類似)。該程式的主要功能是通過回撥呼叫此定時器功能,該回調在終端上列印一個句子。換句話說,這個程式每秒鐘無休止地列印句子“時間飛得像箭頭”。
object Timer {
def oncePerSecond(callback: () => Unit) {
while (true) { callback(); Thread sleep 1000 }
}
def timeFlies() {
println("time flies like an arrow...")
}
def main(args: Array[String]) {
oncePerSecond(timeFlies)
}
}
請注意,為了列印字串,我們使用預定義的方法println
而不是使用的方法System.out
。
匿名函式
雖然這個程式很容易理解,但可以稍微改進一下。首先,請注意該函式timeFlies
僅定義為稍後傳遞給oncePerSecond
函式。必須命名只使用過一次的那個函式,這似乎是不必要的,實際上能夠像傳遞給它一樣構造這個函式真的很好oncePerSecond
。這在Scala中可以使用匿名函式,這正是:沒有名稱的函式。使用匿名函式而不是timeFlies的我們的計時器程式的修訂版看起來像這樣:
object TimerAnonymous {
def oncePerSecond(callback: () => Unit) {
while (true) { callback(); Thread sleep 1000 }
}
def main(args: Array[String]) {
oncePerSecond(() =>
println("time flies like an arrow..."))
}
}
右側箭頭顯示了此示例中匿名函式的存在,該箭頭=>
將函式的引數列表與其正文分開。在此示例中,引數列表為空,如箭頭左側的空對括號所示。該函式的主體與timeFlies
上面的相同。
類
正如我們上面所看到的,Scala是一種面向物件的語言,因此它具有類的概念。(為了完整起見,應該注意一些面向物件的語言不具有類的概念,但Scala不是其中之一。)Scala中的類是使用接近Java語法的語法宣告的。一個重要的區別是Scala中的類可以有引數。這在複數的以下定義中說明。
class Complex(real: Double, imaginary: Double) {
def re() = real
def im() = imaginary
}
這個Complex
類有兩個引數,它們是複合體的實部和虛部。建立類的例項時必須傳遞這些引數Complex
,如下所示:new Complex(1.5, 2.3)
。該類包含兩個名為re
and的方法,im
它們可以訪問這兩個部分。
應該注意,這兩種方法的返回型別沒有明確給出。它將由編譯器自動推斷,編譯器檢視這些方法的右側並推斷它們都返回型別的值Double
。
編譯器並不總是像它在這裡那樣推斷型別,並且遺憾的是沒有簡單的規則來確切知道它何時會發生,何時不會。在實踐中,這通常不是問題,因為編譯器在無法推斷未明確給出的型別時會抱怨。作為一個簡單的規則,初學者Scala程式設計師應該嘗試省略類似的宣告,這些宣告似乎很容易從上下文中推斷出來,看看編譯器是否同意。一段時間後,程式設計師應該很好地瞭解何時省略型別,何時明確指定它們。
沒有引數的方法
的方法,一個小問題re
,並im
是,為了給他們打電話,一個人把一對空括號中他們的名字後,如下例所示:
object ComplexNumbers {
def main(args: Array[String]) {
val c = new Complex(1.2, 3.4)
println("imaginary part: " + c.im())
}
}
如果它們是欄位,那麼能夠訪問真實和虛構部分會更好,而不需要放置空的括號對。這在Scala中是完全可行的,只需將它們定義為沒有引數的方法即可。這些方法與零引數的方法不同,因為它們的名稱後面沒有括號,無論是在定義中還是在它們的使用中。我們的 Complex
課程可以改寫如下:
class Complex(real: Double, imaginary: Double) {
def re = real
def im = imaginary
}
繼承和壓倒一切
Scala中的所有類都繼承自超類。如果沒有指定超類Complex
,scala.AnyRef
則隱式使用前一節的示例 。
可以覆蓋從Scala中的超類繼承的方法。但是,必須明確指定方法使用override
修飾符覆蓋另一個方法,以避免意外覆蓋。作為一個例子,我們的Complex
類可以通過重新定義toString
繼承自的方法來擴充Object
。
class Complex(real: Double, imaginary: Double) {
def re = real
def im = imaginary
override def toString() =
"" + re + (if (im < 0) "" else "+") + im + "i"
}
案例類和模式匹配
通常出現在程式中的一種資料結構是樹。例如,直譯器和編譯器通常將程式內部表示為樹; XML文件是樹; 幾種容器都是以樹木為基礎,如紅黑樹。
我們現在將研究如何通過一個小型計算器程式在Scala中表示和操作這些樹。該程式的目的是操縱由和,整數常量和變數組成的非常簡單的算術表示式。這種表達的兩個例子是 1+2
和(x+x)+(7+y)
。
我們首先必須決定這種表達的表示。最自然的是樹,其中節點是操作(這裡是新增),葉子是值(這裡是常量或變數)。
在Java中,這樣的樹將使用樹的抽象超類來表示,並且每個節點或葉使用一個具體的子類。在函數語言程式設計語言中,可以使用代數資料型別來實現相同的目的。Scala提供了案例類的概念, 它們介於兩者之間。以下是它們如何用於為我們的示例定義樹的型別:
abstract class Tree
case class Sum(l: Tree, r: Tree) extends Tree
case class Var(n: String) extends Tree
case class Const(v: Int) extends Tree
那類的事實Sum
,Var
且Const
被宣告為case類意味著他們從標準類的區別在以下幾個方面:
- 該
new
關鍵字不是強制性的,以建立這些類(即,一個可以寫入的情況下Const(5)
,而不是new Const(5)
), - 為建構函式引數自動定義getter函式(即,可以通過寫入獲取
v
某個c
類例項的建構函式引數的值),Const
c.v
- 對於方法的預設定義
equals
和hashCode
設定,在其工作結構的情況下,並沒有對他們的身份, toString
提供了方法的預設定義,並以“源表單”(例如,表示式x+1
列印的樹列印為Sum(Var(x),Const(1))
)列印該值 ,- 這些類的例項可以通過模式匹配進行分解, 如下所示。
現在我們已經定義了資料型別來表示我們的算術表示式,我們可以開始定義操作來操作它們。我們將從一個函式開始,在某些環境中評估表示式 。環境的目的是為變數賦值。例如,在x+1
將值5
與變數x
(寫入) 相關聯的環境中計算的表示式作為結果{ x -> 5 }
給出6
。
因此,我們必須找到一種表示環境的方法。我們當然可以使用一些像雜湊表這樣的關聯資料結構,但我們也可以直接使用函式!環境實際上只是一個將值與(變數)名稱相關聯的函式。{ x -> 5 }
上面給出的環境可以簡單地在Scala中編寫如下:
{ case "x" => 5 }
這種表示法定義了一個函式,當給定字串 "x"
作為引數時,它返回整數5
,否則失敗,但異常。
在編寫評估函式之前,讓我們給出環境型別的名稱。當然,我們總是可以將型別String => Int
用於環境,但如果我們為此型別引入名稱,它會簡化程式,並使未來的更改更容易。這是在Scala中使用以下符號完成的:
type Environment = String => Int
從此,該型別Environment
可被用作功能從型別的別名String
來Int
。
我們現在可以給出評估函式的定義。從概念上講,它非常簡單:兩個表示式之和的值只是這些表示式的值的總和; 變數的值直接從環境中獲得; 而常數的值本身就是常數。在Scala中表達這一點並不困難:
def eval(t: Tree, env: Environment): Int = t match {
case Sum(l, r) => eval(l, env) + eval(r, env)
case Var(n) => env(n)
case Const(v) => v
}
此評估功能通過 在樹上執行模式匹配來工作t
。直觀地說,上述定義的含義應該是清楚的:
- 它首先檢查樹
t
是否為aSum
,如果是,它將左子樹繫結到一個名為的新變數l
,將右子樹繫結到一個被呼叫的變數r
,然後繼續按箭頭的方式評估表示式; 該表示式可以(並且不會)使用由出現在箭頭的左側,即,圖案繫結的變數,l
並且r
, - 如果第一次檢查沒有成功,也就是說,如果樹不是a
Sum
,則繼續檢查是否t
為aVar
; 如果是,它將Var
節點中包含的名稱繫結到變數n
並繼續使用右側表示式, - 如果第二次檢查也失敗,即if
t
既不是aSum
也不是aVar
,它檢查它是否為aConst
,如果是,則將Const
節點中包含的值繫結到變數v
並繼續右側, - 最後,如果所有檢查都失敗,則會引發異常以指示模式匹配表示式的失敗; 只有
Tree
在聲明瞭更多的子類時,才會發生這種情況。
我們看到模式匹配的基本思想是嘗試將值與一系列模式匹配,並且只要模式匹配,提取並命名值的各個部分,最後評估一些通常使用這些模式的程式碼。命名部分。
作為一名經驗豐富的面向物件的程式設計人員可能會問,為什麼我們沒有定義eval
的方法類Tree
和它的子類。我們本可以做到這一點,因為Scala允許在案例類中使用方法定義,就像在普通類中一樣。因此,決定是否使用模式匹配或方法是一種品味問題,但它對可擴充套件性也有重要影響:
- 在使用方法時,很容易新增一種新節點,因為這可以通過
Tree
為它定義一個子類來完成; 另一方面,新增一個操作樹的新操作是繁瑣的,因為它需要修改所有子類Tree
, - 在使用模式匹配時,情況正好相反:新增新型別的節點需要修改在樹上進行模式匹配的所有函式,以考慮新節點; 另一方面,通過將其定義為獨立函式,新增新操作很容易。
為了進一步探索模式匹配,讓我們在算術表示式上定義另一個操作:符號派生。讀者可能會記住有關此操作的以下規則:
- 和的導數是導數的總和,
- 某個變數的導數
v
是1,如果v
是相對於該導數發生的變數,則為0,否則為0 - 常數的導數為零。
這些規則幾乎可以翻譯成Scala程式碼,以獲得以下定義:
def derive(t: Tree, v: String): Tree = t match {
case Sum(l, r) => Sum(derive(l, v), derive(r, v))
case Var(n) if (v == n) => Const(1)
case _ => Const(0)
}
該函式引入了兩個與模式匹配相關的新概念。首先,case
變數的表示式有一個guard,一個跟在if
關鍵字後面的表示式。除非其表示式為真,否則此保護可防止模式匹配成功。這裡它用於確保1
只有在派生變數的名稱與派生變數相同時才返回常量v
。這裡使用的模式匹配的第二個新特性是寫入的萬用字元_
,它是匹配任何值的模式,而不給它命名。
我們還沒有探索模式匹配的全部功能,但我們將在此處停下來以保持此文件的簡短性。我們仍然希望看到上面兩個函式如何在一個真實的例子上執行。為了該目的,讓我們編寫一個簡單的main
功能,其對錶達幾種操作(x+x)+(7+y)
:它首先計算其在環境中的值{ x -> 5, y -> 7 }
,那麼它的衍生物相對計算到x
,然後y
。
def main(args: Array[String]) {
val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
val env: Environment = { case "x" => 5 case "y" => 7 }
println("Expression: " + exp)
println("Evaluation with x=5, y=7: " + eval(exp, env))
println("Derivative relative to x:\n " + derive(exp, "x"))
println("Derivative relative to y:\n " + derive(exp, "y"))
}
您將需要包裝的Environment
型別和eval
,derive
以及 main
在方法Calc
編譯前的物件。執行此程式,我們得到預期的輸出:
Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y)))
Evaluation with x=5, y=7: 24
Derivative relative to x:
Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0)))
Derivative relative to y:
Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))
通過檢查輸出,我們看到衍生的結果應該在呈現給使用者之前簡化。使用模式匹配定義基本的簡化函式是一個有趣(但令人驚訝的棘手)問題,留給讀者練習。
性狀
除了從超類繼承程式碼之外,Scala類還可以從一個或多個特徵匯入程式碼。
也許Java程式設計師理解特徵的最簡單方法是將它們視為可以包含程式碼的介面。在Scala中,當一個類繼承自trait時,它實現了該trait的介面,並繼承了trait中包含的所有程式碼。
為了看到特徵的有用性,讓我們看一個經典的例子:有序物件。能夠比較給定類之間的物件(例如對它們進行排序)通常很有用。在Java中,可比較的物件實現Comparable
介面。在Scala中,通過定義我們Comparable
稱之為特徵的 等價物,我們可以比Java更好一些Ord
。
比較物件時,六個不同的謂詞可能很有用:更小,更小或相等,相等,不相等,更大或更大,以及更大。然而,定義所有這些都是挑剔的,特別是因為這六個中的四個可以使用剩下的兩個來表達。也就是說,給定相等和較小的謂詞(例如),可以表達其他謂詞。在Scala中,以下特徵宣告可以很好地捕獲所有這些觀察結果:
trait Ord {
def < (that: Any): Boolean
def <=(that: Any): Boolean = (this < that) || (this == that)
def > (that: Any): Boolean = !(this <= that)
def >=(that: Any): Boolean = !(this < that)
}
這個定義建立了一個名為的新型別Ord
,它與Java的Comparable
介面扮演相同的角色,並且根據第四個抽象概念建立三個謂詞的預設實現。平等和不平等的謂詞不會出現在此處,因為它們預設存在於所有物件中。
的型別Any
,其上面使用是這是一種超級型所有其他型別Scala中的型別。它可以被看作是Java的更一般的版本Object
型別,因為它也是一個超級型別的基本型別,如Int
,Float
等。
為了使類的物件具有可比性,因此足以定義測試相等性和低劣性的謂詞,並在Ord
上面的類中進行混合。例如,讓我們定義一個 Date
表示公曆中日期的類。這些日期由一天,一個月和一年組成,我們都將整數表示為整數。因此,我們開始對Date
類的定義 如下:
class Date(y: Int, m: Int, d: Int) extends Ord {
def year = y
def month = m
def day = d
override def toString(): String = year + "-" + month + "-" + day}
這裡的重要部分是extends Ord
遵循類名和引數的宣告。它宣告 Date
該類繼承了Ord
特徵。
然後,我們重新定義equals
繼承自的方法, Object
以便通過比較各個欄位來正確比較日期。預設實現equals
不可用,因為在Java中它會物理地比較物件。我們得出以下定義:
override def equals(that: Any): Boolean =
that.isInstanceOf[Date] && {
val o = that.asInstanceOf[Date]
o.day == day && o.month == month && o.year == year
}
此方法使用預定義的方法isInstanceOf
和asInstanceOf
。第一個,isInstanceOf
對應於Java的instanceof
運算子,當且僅當應用它的物件是給定型別的例項時才返回true。第二個asInstanceOf
對應於Java的強制轉換操作符:如果物件是給定型別的例項,則將其視為此類,否則ClassCastException
丟擲a。
最後,定義的最後一個方法是測試劣勢的謂詞,如下所示。它使用另一個方法,error
從package物件scala.sys
中丟擲給定錯誤訊息的異常。
def <(that: Any): Boolean = {
if (!that.isInstanceOf[Date])
sys.error("cannot compare " + that + " and a Date")
val o = that.asInstanceOf[Date]
(year < o.year) ||
(year == o.year && (month < o.month ||
(month == o.month && day < o.day)))
}
這樣就完成了Date
類的定義。可以將此類的例項視為日期或類似物件。此外,它們都限定上面提到的六個比較謂詞:equals
和<
因為它們直接出現在定義Date
的類,和別人是因為它們被從遺傳Ord
性狀。
當然,特徵在除此處所示的情況之外的其他情況下很有用,但是長度討論它們的應用程式超出了本文件的範圍。
泛型
我們將在本教程中探討的Scala的最後一個特性是通用性。Java程式設計師應該清楚地知道他們的語言缺乏通用性所帶來的問題,這是Java 1.5中解決的一個缺點。
通用性是編寫按型別引數化的程式碼的能力。例如,為連結串列編寫庫的程式設計師面臨著決定給列表元素賦予哪種型別的問題。由於此列表旨在用於許多不同的上下文中,因此不可能確定元素的型別必須是 Int
。這將完全是武斷的,而且過於嚴格。
Java程式設計師訴諸於使用Object
,這是所有物件的超型別。該解決方案然而被很不理想,因為它沒有為基本型別的工作(int
, long
,float
等),這意味著大量的動態型別的強制轉換必須由程式設計師插入。
Scala可以定義泛型類(和方法)來解決這個問題。讓我們用一個最簡單的容器類的例子來檢查這個:引用,它可以是空的,也可以指向某種型別的物件。
class Reference[T] {
private var contents: T = _
def set(value: T) { contents = value }
def get: T = contents
}
該類Reference
由一個名為引數化的型別呼叫T
,該型別是其元素的型別。此型別在類的主體中用作contents
變數的型別,set
方法的引數和方法的返回型別get
。
上面的程式碼示例在Scala中引入了變數,不需要進一步解釋。然而,有趣的是_
,給予該變數的初始值是,表示預設值。該預設值為0數值型別, false
對於Boolean
型別,()
在Unit
型別和null
所有物件型別。
要使用此類Reference
,需要指定要用於type引數T
的型別,即單元格包含的元素的型別。例如,要建立和使用包含整數的單元格,可以編寫以下內容:
object IntegerReference {
def main(args: Array[String]) {
val cell = new Reference[Int]
cell.set(13)
println("Reference contains the half of " + (cell.get * 2))
}
}
從該示例中可以看出,get
在將其用作整數之前,不必轉換方法返回的值。也不可能在該特定單元格中儲存除整數之外的任何內容,因為它被宣告為包含整數。
結論
本文件簡要概述了Scala語言並提供了一些基本示例。感興趣的讀者可以繼續,例如,閱讀文件Scala By Example,其中包含更多高階示例,並在需要時參考Scala語言規範。