摘要Java;開發人員可以將對象作為理解 Scala 的出發點本文是面向 Java 開發人員的 Scala 指南 系列 的第二期作者 Ted Neward 遵循對一種語言進行評價的基本前提一種語言的威力可以直接通過它集成新功能的能力衡量在本文中就是指對復數的支持跟隨本文您將了解在 Scala 中與類的定義和使用有關的一些有趣特性
迄今為止在此 系列 中我們已經討論了 Scala 對生態環境的保真度展示了 Scala 如何將眾多的 Java 核心對象功能合並在一起如果 Scala 只是編寫對象的另一種方式那麼它不會有任何引人注意的地方或者說不再那麼功能強大Scala 的函數概念和對象概念的合並以及它對編程人員效率的重視這些使得學習 Scala 語言比 JavacumScala 編程人員所想象的體驗更加復雜更加微妙
關於本系列
Ted Neward 潛心研究 Scala 編程語言並帶您跟他一起徜徉在這個新的 developerWorks 系列 中您將深入了解 Scala並在實踐中看到 Scala 的語言功能在進行相關比較時Scala 代碼和 Java 代碼將放在一起展示但(您將發現)Scala 中的許多內容與您在 Java 編程中發現的任何內容都沒有直接關聯而這正是 Scala 的魅力所在!畢竟如果 Java 代碼可以做到的話又何必學習 Scala 呢?
例如對控制結構(比如 ifwhile 和 for)使用 Scala 的方法盡管這些控制結構看起來類似一些老的還比較不錯的 Java 結構但實際上 Scala 為它們增加了一些完全不同的特性本月的文章是關於使用 Scala 控制結構時能夠期望獲得哪些東西的入門級讀物而不是在制造許多錯誤(並編寫一堆錯誤代碼)之後讓您冒著遭受挫折的風險去尋找差異
修訂後的 Personscala
在 本系列的上一篇文章 中可以了解到 Scala 能夠通過定義一些方法來定義 POJO這些方法模仿基於 POJO 的環境所需的傳統 getter 和 setter在這篇文章發表之後我收到了 Bill Venners 發來的電子郵件Bill Venners 是即將發表的正式的 Scala 參考資料使用 Scala 編程(請參閱 參考資料)的合著者之一Bill 指出了實現上述操作的一個更簡單的方法即使用 scalareflectBeanProperty 標注如下所示
清單 修改後的 Personscala
class Person(fn:String ln:String a:Int)
{
@scalareflectBeanProperty
var firstName = fn
@scalareflectBeanProperty
var lastName = ln
@scalareflectBeanProperty
var age = a
override def toString =
[Person firstName: + firstName + lastName: + lastName +
age: + age + ]
}
清單 中的方法(上一篇文章 中的清單 的修訂版)為指定的 var 生成了 get/set 方法對惟一的缺陷是這些方法並不實際存在於 Scala 代碼中因此其他 Scala 代碼無法調用它們這通常不是什麼大問題因為 Scala 將對為自己生成的字段使用已生成的方法如果事先不知道那麼這些對您而言可能是一個驚喜
在查看了清單 中的代碼之後最讓我感到震動的是Scala 並沒有只演示組合函數概念和對象概念的強大威力它還演示了自 Java 首次發布之後的 年裡對象語言帶來的一些益處
控制是一種幻想
您將看到的許多奇怪的不可思議的東西都可以歸功於 Scala 的函數特性因此簡單介紹一下函數語言開發和演變的背景可能非常有用
在函數語言中將越來越高級的結構直接構建到語言中是不常見的此外語言是通過一組核心原語結構定義的在與將函數作為對象傳遞的功能結合之後可用來定義功能的高階函數 看起來 像是超出了核心語言的范圍但實際上它只是一個庫類似於任何庫此功能可以替換擴充或擴展
根據一組核心原語構建語言的合成 特性由來已久可以追溯到 世紀 年代和 年代使用 SmalltalkLisp 和 Scheme 的時候諸如 Lisp 和 Scheme 之類的語言因為它們在更低級別的抽象上定義更高級別抽象的能力而受到人們的狂熱追捧編程人員可以使用高級抽象用它們構建更高級的抽象如今聽到討論這個過程時它通常是關於特定於域的語言(或 DSL)的(請參閱 參考資料)實際上它只是關於如何在抽象之上構建抽象的過程
在 Java 語言中惟一選擇就是利用 API 調用完成此操作在 Scala 中可以通過擴展語言本身實現它試圖擴展 Java 語言會帶來創建極端場景(corner case)的風險這些場景將威脅全局的穩定性而試圖擴展 Scala 則只意味著創建一個新庫
If 結構
我們將從傳統的 if 結構開始 —— 當然此結構必須是最容易處理的結構之一不是嗎?畢竟從理論上說if 只檢查一個條件如果條件為真則執行後面跟著的代碼
但是這種簡單性可能帶有欺騙性傳統上Java 語言對 if 的 else 子句的使用是隨意的並且假定如果條件出錯可以只跳過代碼塊但在函數語句中情況不是這樣為了保持函數語句的算術特性所有一切都必須以表達式計算的方式出現包括 if 子句本身(對於 Java 開發人員這正是三元操作符 —— ? 表達式 —— 的工作方式)
在 Scala 中非真代碼塊(代碼塊的 else 部分)必須以與 if 代碼塊中值種類相同的形式呈現並且必須產生同一種類的值這意味著不論以何種方式執行代碼總會產生一個值例如請參見以下 Java 代碼
清單 哪個配置文件?(Java 版)
// This is Java
String filename = defaultproperties;
if (ntains(configFile))
filename = (String)optionsget(configFile);
因為 Scala 中的 if 結構自身就是一個表達式所以重寫上述代碼會使它們成為清單 中所示的更正確的代碼片段
清單 哪個配置文件?(Scala 版)
// This is Scala
val filename =
if (ntains(configFile))
optionsget(configFile)
else
defaultproperties
val 與 var
您可能想更多地了解 val
與 var
之間的不同實際上它們的不同之處在於 —— 一個是只讀的值另一個是可變的變量通常函數語言特別是被認為是 純 函數語言(不允許帶有副作用比如可變狀態)的那些函數語言只支持 val
概念但是因為 Scala 要同時吸引函數編程人員和命令/對象編程人員所以這二種結構它都提供
也就是說Scala 編程人員通常應該首選 val
結構並在明確需要可變性的時候選擇 var
原因很簡單除了使編程更容易之外val
還能確保程序的線程安全性Scala 中的一個內在主題是幾乎每次認為需要可變狀態時其實都不需要可變狀態讓我們從不可變字段和本地變量(val
)開始這是展示上述情況的一種方法甚至對最堅定的 Java 懷疑論者也是如此從 Java 中的 final
開始介紹可能不是很合理或許是因為 Java 的非函數特性盡管此原因不可取一些好奇的 Java 開發人員可能想嘗試一下
盡管真正的贏家是 Scala但可以通過編寫代碼將結果分配給 val而不是 var在設置之後就無法對 val 進行更改這與 Java 語言中 final 變量的操作方式是相同的不可變本地變量最顯著的副作用是很容易實現並發性試圖用 Java 代碼實現同樣的操作時會帶來許多不錯的易讀的好代碼如清單 中所示
清單 哪個配置文件?(Java 版三元式)
//This is Java
final String filename =
ntains(configFile) ?
optionsget(configFile) : defaultproperties;
用代碼評審解釋這一點可能需要點技巧也許這樣做是正確的但許多 Java 編程人員會不以為然並且詢問 您做那個干什麼?
已公開的 while 結構
接下來讓我們來看一下 while 及其同胞 dowhile它們做的基本上是同一件事測試一個條件如果該條件為真則繼續執行提供的代碼塊
通常函數語言會避開 while 循環因為 while 實現的大多數操作都可以使用遞歸來完成函數語言真地非常類似於 遞歸例如可以考慮一下 Scala by Example(請參閱 參考資料)中展示的 quicksort 實現該實現可以與 Scala 實現一起使用
清單 Quicksort(Java 版)
//This is Java
void sort(int[] xs) {
sort(xs xslength );
}
void sort(int[] xs int l int r) {
int pivot = xs[(l+r)/];
int a = l; int b = r;
while (a <= b)
while (xs[a] < pivot) { a = a + ; }
while (xs[b] > pivot) { b = b 每 ; }
if (a <= b) {
swap(xs a b);
a = a + ;
b = b 每 ;
}
}
if (l < b) sort(xs l b);
if (b < r) sort(xs a r);
}
void swap(int[] arr int i int j) {
int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}
不必深入太多的細節就可以了解 while 循環的用法它是通過數組中的各種元素進行迭代的先找到一個支點然後依次對每個子元素進行排序毫不令人奇怪的是while 循環也需要一組可變本地變量在這裡這些變量被命名為 a 和 b其中存儲的是當前支點注意此版本甚至可以在循環自身中使用遞歸兩次調用循環本身一次用於對列表左手邊的內容進行排序另一次對列表右手邊的內容進行排序
這足以說明清單 中的 quicksort 真的不太容易讀取更不用說理解它現在來考慮一下 Scala 中的直接 等同物(這意味著該版本與上述版本盡量接近)
清單 Quicksort(Scala 版)
//This is Scala
def sort(xs: Array[Int]) {
def swap(i: Int j: Int) {
val t = xs(i); xs(i) = xs(j); xs(j) = t
}
def sort(l: Int r: Int) {
val pivot = xs((l + r) / )
var i = l; var j = r
while (i <= j) {
while (xs(i) < pivot) i +=
while (xs(j) > pivot) j =
if (i <= j) {
swap(i j)
i +=
j =
}
}
if (l < j) sort(l j)
if (j < r) sort(i r)
}
sort( xslength )
}
清單 中的代碼看起來非常接近於 Java 版也就是說該代碼很長很難看並且難以理解(特別是並發性那一部分)明顯不具備 Java 版的一些優點
So Ill improve it ……
清單 Quicksort(更好的 Scala 版)
//This is Scala
def sort(xs: Array[Int]): Array[Int] =
if (xslength <= ) xs
else {
val pivot = xs(xslength / )
ncat(
sort(xs filter (pivot >))
xs filter (pivot ==)
sort(xs filter (pivot <)))
}
顯然清單 中的 Scala 代碼更簡單一些注意遞歸的使用避免完全 while 循環可以對 Array 類型使用 filter 函數從而對其中的每個元素應用 greaterthanequals 和 lessthan 函數事實上在引導裝入程序之後因為 if 表達式是返回某個值的表達式所以從 sort() 返回的是 sort() 的定義中的(單個)表達式
簡言之我已經將 while 循環的可變狀態完全再次分解為傳遞給各種 sort() 調用的參數 —— 許多 Scala 狂熱愛好者認為這是編寫 Scala 代碼的正確方式
可能值得一提的是Scala 本身並不介意您是否使用 while 代替迭代 —— 您會看到來自編譯器的 您在干什麼在做蠢事嗎? 的警告Scala 也不會阻止您在可變狀態下編寫代碼但是使用 while 或可變狀態意味著犧牲 Scala 語言的另一個關鍵方面即鼓勵編寫具有良好並行性的代碼只要有可能並且可行Scala 式作風 會建議您優先在命令塊上執行遞歸
編寫自己的語言結構
我想走捷徑來討論一下 Scala 的控制結構做一些大多數 Java 開發人員根本無法相信的事 —— 創建自己的語言結構
那些通過死讀書學習語言的書呆子會發現一件有趣的事while 循環(Scala 中的一個原語結構)可能只是一個預定義函數Scala 文檔以及假設的 While 定義中對此進行了解釋說明
// This is Scala
def While (p: => Boolean) (s: => Unit) {
if (p) { s ; While(p)(s) }
}
上述語句指定了一個表達式該表達式產生了一個布爾值和一個不返回任何結果的代碼塊(Unit)這正是 while 所期望的
擴展這些代碼行很容易並且可以根據需要使用它們只需導入正確的庫即可正如前面提到的這是構建語言的綜合方法在下一節介紹 try 結構的時候請將這一點牢記於心
再三嘗試
try 結構允許編寫如下所示代碼
清單 如果最初沒有獲得成功……
// This is Scala
val url =
try {
new URL(possibleURL)
}
catch {
case ex: MalformedURLException =>
new URL()
}
清單 中的代碼與 清單 或 清單 中 if 示例中的代碼相差甚遠實際上它比使用傳統 Java 代碼編寫更具技巧特別是在您想捕獲不可變位置上存儲的值的時候(正如我在 清單 中最後一個示例中所做的那樣)這是 Scala 的函數特性的又一個優點!
清單 中所示的 case ex 語法是另一個 Scala 結構(匹配表達式)的一部分該表達式用於 Scala 中的模式匹配我們將研究模式匹配這是函數語言的一個常見特性稍後將介紹它現在只把它看作一個將用於 switch/case 的概念那麼哪種 C 風格的 struct 將用於類呢?
現在再來考慮一下異常處理眾所周知Scala 支持異常處理是因為它是一個表達式但開發人員想要的是處理異常的標准方法並不僅僅是捕獲異常的能力在 AspectJ 中是通過創建方面(aspect)來實現這一點的這些方面圍繞代碼部分進行聯系它們是通過切入點定義的如果想讓數據庫的不同部分針對不同種類異常采取不同行為那麼必須小心編寫這些切入點 —— SQLExceptions 的處理應該不同於 IOExceptions 的處理依此類推
在 Scala 中這只是微不足道的細節請留神觀察!
清單 一個自定義異常表達式
// This is Scala
object Application
{
def generateException()
{
Systemoutprintln(Generating exception);
throw new Exception(Generated exception);
}
def main(args : Array[String])
{
tryWithLogging // This is not part of the language
{
generateException
}
Systemoutprintln(Exiting main());
}
def tryWithLogging (s: => _) {
try {
s
}
catch {
case ex: Exception =>
// where would you like to log this?
// I choose the console window for now
exprintStackTrace()
}
}
}
與前面討論過的 While 結構類似tryWithLogging 代碼只是來自某個庫的函數調用(在這裡是來自同一個類)可以在適當的地方使用不同的主題變量不必編寫復雜的切入點代碼
此方法的優點在於它利用了 Scala 的捕獲一級結構中橫切邏輯的功能 —— 以前只有面向方面的人才能對此進行聲明清單 中的一級結構捕獲了一些異常(經過檢查的和未經檢查的都包括)並以特定方式進行處理上述想法的副作用非常多惟一的限制也許就是想象力了您只需記得 Scala 像許多函數語言一樣允許使用代碼塊(aka 函數)作為參數並根據需要使用它們即可
for 生成語言
所有這些都引導我們來到了 Scala 控制結構套件的實際動力源泉for 結構該結構看起來像是 Java 的增強 for 循環的簡單早期版但它遠比一般的 Java 編程人員開始設想的更強大
讓我們來看一下 Scala 如何處理集合上的簡單順序迭代根據您的 Java 編程經驗我想您應該非常清楚該怎麼做
清單 對一個對象使用 for 循環和對所有對象使用 for 循環
// This is Scala
object Application
{
def main(args : Array[String])
{
for (i < to ) // the leftarrow means assignment in Scala
Systemoutprintln(Counting + i)
}
}
此代碼所做的正如您期望的那樣循環 次並且每次都輸出一些值需要小心的是表達式 to 並不意味著 Scala 內置了整數感知(awareness of integer)以及從 到 的計數方式從技術上說這裡存在一些更微妙的地方編譯器使用 Int 類型上定義的方法 to 生成一個 Range 對象(Scala 中的任何東西都是對象還記得嗎?)該對象包含要迭代的元素如果用 Scala 編譯器可以看見的方式重新編寫上述代碼那麼該代碼看起來很可能如下所示
清單 編譯器看見的內容
// This is Scala
object Application
{
def main(args : Array[String])
{
for (i < to()) // the leftarrow means assignment in Scala
Systemoutprintln(Counting + i)
}
}
實際上Scala 的 for 並不了解那些成員並且並不比其他任何對象類型做得更好它所了解的是 scalaIterablescalaIterable 定義了在集合上進行迭代的基本行為提供 Iterable 功能(從技術上說它是 Scala 中的一個特征但現在將它視為一個接口)的任何東西都可以用作 for 表達式的核心ListArray甚至是您自己的自定義類型都可以在 for 中使用
特殊性
正如上面已經證明的那樣for 循環可以做許多事情並不只是遍歷可迭代的項列表事實上可以使用一個 for 循環在操作過程中過濾許多項並在每個階段都產生一個新列表
讓 Scala 與英語更接近
您可能已經注意到理解清單 中的 Scala 的 for
循環版本更容易一些這要感謝 Range
對象暗中將兩端都包含在內以下英語語言語法比 Java 語言更接近些假如有一條 Range
語句說 from to do this那麼這意味著不再產生意外的 offbyone 錯誤
清單 看一看還有哪些優點
// This is Scala
object Application
{
def main(args : Array[String])
{
for (i < to ; i % == )
Systemoutprintln(Counting + i)
}
}
注意到清單 中 for 表達式的第二個子句了嗎?它是一個過濾器實際上只有那些傳遞給過濾器(即計算 true)的元素 向前傳給 了循環主體在這裡只輸出了 到 的偶數數字
並不要求 for 表達式的各個階段都成為過濾器您甚至可以將一些完全平淡無奇的東西(從循環本身的觀點來看)放入管道中例如以下代碼顯示了在下一個階段進行計算之前的 i 的當前值
清單 讓我如何愛上您呢?別那麼冗長
// This is Scala
object App
{
def log(item : _) : Boolean =
{
Systemoutprintln(Evaluating + item)
true
}
def main(args : Array[String]) =
{
for (val i < to ; log(i); (i % ) == )
Systemoutprintln(Counting + i)
}
}
在運行的時候范圍 到 中的每個項都將發送給 log它將通過顯式計算每個項是否為 true 來 批准 每個項然後for 的第三個子句將對這些項進行篩選過濾出那些滿足是偶數的條件的元素因此只將偶數傳遞給了循環主體本身
簡單性
在 Scala 中可以將 Java 代碼中復雜的一長串語句縮短為一個簡單的表達式例如以下是遍歷目錄查找所有 scala 文件並顯示每個文件名稱的方法
清單 Finding scala
// This is Scala
object App
{
def main(args : Array[String]) =
{
val filesHere = (new javaioFile())listFiles
for (
file < filesHere;
if fileisFile;
if filegetNameendsWith(scala)
) Systemoutprintln(Found + file)
}
}
這種 for 過濾很常見(並且在此上下文中分號很讓人討厭)使用這種過濾是為了幫助您做出忽略分號的決定此外Scala 允許將上述示例中的圓括號之間的語句直接作為代碼塊對待
清單 Finding scala(版本 )
// This is Scala
object App
{
def main(args : Array[String]) =
{
val filesHere = (new javaioFile())listFiles
for {
file < filesHere
if fileisFile
if filegetNameendsWith(scala)
} Systemoutprintln(Found + file)
}
}
作為 Java 開發人員您可能發現最初的圓括號加分號的語法更直觀一些沒有分號的曲線括號語法很難讀懂幸運的是這兩種句法產生的代碼是等效的
一些有趣的事
在 for 表達式的子句中可以分配一個以上的項如清單 中所示
清單 名稱中有什麼?
// This is Scala
object App
{
def main(args : Array[String]) =
{
// Note the arrayinitialization syntax; the type (Array[String])
// is inferred from the initialized elements
val names = Array(Ted Neward Neal Ford Scott Davis
Venkat Subramaniam David Geary)
for {
name < names
firstName = namesubstring( nameindexOf( ))
} Systemoutprintln(Found + firstName)
}
}
這被稱為 中途賦值(midstream assignment)其工作原理如下定義了一個新值 firstName該值用於保存每次執行循環後的 substring 調用的值以後可以在循環主體中使用此值
這還引出了嵌套 迭代的概念所有迭代都位於同一表達式中
清單 Scala grep
// This is Scala
object App
{
def grep(pattern : String dir : javaioFile) =
{
val filesHere = dirlistFiles
for (
file < filesHere;
if (filegetNameendsWith(scala) || filegetNameendsWith(java));
line < scalaioSourcefromFile(file)getLines;
if linetrimmatches(pattern)
) println(line)
}
def main(args : Array[String]) =
{
val pattern = *object*
grep pattern new javaioFile()
}
}
在此示例中grep 內部的 for 使用了兩個嵌套迭代一個在指定目錄(其中每個文件都與 file 連接在一起)中找到的所有文件上進行迭代另一個迭代在目前正被迭代的文件(與 line 本地變量連接在一起)中發現的所有行上進行迭代
使用 Scala 的 for 結構可以做更多的事但目前為止提供的示例已足以表達我的觀點Scala 的 for 實際上是一條管道它在將元素傳遞給循環主體之前處理元素組成的集合每次一個此管道其中的一部分負責將更多的元素添加到管道中(生成器)一部分負責編輯管道中的元素(過濾器)還有一些負責處理中間的操作(比如記錄)無論如何Scala 會帶給您與 Java 中引入的 增強的 for 循環 不同的體驗
匹配
今天要了解的最後一個 Scala 控制結構是 match它提供了許多 Scala 模式匹配功能幸運的是模式匹配會聲明對某個值進行計算的代碼塊首先將執行代碼塊中最接近的匹配結果因此在 Scala 中可以包含以下代碼
清單 一個簡單的匹配
// This is Scala
object App
{
def main(args : Array[String]) =
{
for (arg < args)
arg match {
case Java => println(Java is nice)
case Scala => println(Scala is cool)
case Ruby => println(Ruby is for wimps)
case _ => println(What are you a VB programmer?)
}
}
}
剛開始您可能將 Scala 模式匹配設想為支持 String 的 開關帶有通常用作通配符的下劃線字符而這正是典型開關中的默認情況但是這樣想會極大地低估該語言模式匹配是許多(但不是大多數)函數語言中可以找到的另一個特性它提供了一些有用的功能
對於初學者(盡管這沒什麼好奇怪的)可能認為 match 表達式自身會產生一個值該值可能出現在賦值語句的右邊正如 if 和 try 語句所做的那樣這一點本身也很有用但匹配的真正威力體現在基於各種類型進行匹配時而不是如上所述匹配單個類型的值或者更多的時候它是兩種匹配的組合
因此假設您有一個聲明返回 Object 的函數或方法 —— 在這裡Java 的 javalangreflectMethodinvoke() 方法的結果可能是一個好例子通常在使用 Java 語言計算結果時首先應該確定其類型但在 Scala 中可以使用模式匹配簡化該操作
清單 您是什麼?
//This is Scala
object App
{
def main(args : Array[String]) =
{
// The Any type is exactly what it sounds like: a kind of wildcard that
// accepts any type
def describe(x: Any) = x match {
case => five
case true => truth
case hello => hi!
case Nil => the empty list
case _ => something else
}
println describe()
println describe(hello)
}
}
因為 match 的很容易簡單明了地描述如何針對各種值和類型進行匹配的能力模式匹配常用於解析器和解釋器中在那裡解析流中的當前標記是與一系列可能的匹配子句匹配的然後將針對另一系列子句應用下一個標記依此類推(注意這也是使用函數語言編寫許多語言解析器編譯器和其他與代碼有關的工具的部分原因這些函數語言中包括 Haskell 或 ML)
關於模式匹配還有許多可說的東西但這些會將我們直接引導至 Scala 的另一個特性 case 類我想將它留到下次再介紹
結束語
Scala 在許多方面看起來都非常類似於 Java但實際上只有 for 結構存在一些相似性核心語法元素的函數特性不僅提供了一些有用的特性(比如已經提到的賦值功能)還提供了使用新穎有趣的方式擴展語言的能力不必修改核心 javac 編譯器本身這使該語言更加符合 DSL 的定義(這些 DSL 是在現有語言的語法中定義的)並且更加符合編程人員根據一組核心原語(a la Lisp 或 Scheme)構建抽象的願望
關於 Scala有如此多的內容可以談論但我們這個月的時間已經用完了記得試用最新的 Scala bits(在撰寫本文時是 final)並嘗試提供的示例感受一下該語言的操作(請參閱 參考資料)請記住到下一次的時候Scala 會將一些有趣的(函數)特性放入編程中!
From:http://tw.wingwit.com/Article/program/Java/hx/201311/25727.html