有個問題一直困擾着 Scala 社區,爲何一些 Java 開發者將 Scala 捧到了天上,認爲它是來自上帝之吻的完美語言;而另一些 Java 開發者卻對它望而卻步,認爲它過於複雜而難以理解。一樣是 Java 開發者,爲什麼會出現兩種大相徑庭的態度,我想這其中必定有誤會。Scala 是一粒金子,可是被一些表面上看起來很是複雜的概念或語法包裹的太嚴實,以致於人們很難在短期內搞清楚它的價值。與此同時,Java 也在不斷地摸索前進,可是因爲 Java 揹負了沉重的歷史包袱,因此每向前一步都顯得異常艱難。本文主要面向 Java 開發人員,但願從解決 Java 中實際存在的問題出發,梳理最容易吸引 Java 開發者的一些 Scala 特性。但願能夠幫助你們快速找到那些真正能夠打動你的點。java
挑逗指數: 五星正則表達式
在 Java 中,針對引用類型,==
用於比較引用相等性,即比較兩個引用變量是否指向同一個對象。因此一個簡單的字符串比較顯得很是囉嗦:算法
String str = new String("Jack"); //錯誤寫法 if (str == "Jack") {} //正確寫法 if (str != null && str.equals("Jack")) {} //或簡寫成 if ("Jack".equals(str)) {}
而在 Scala 中,==
被設計用於值比較,在底層實現上會調用 ==
左邊對象上的 equals
方法,而且會自動處理 null
狀況。因此在 Scala 中,你能夠放心地使用 ==
進行值比較:數據庫
val str = new String("Jack") if (str == "Jack") {}
在平常開發中,值比較的需求要遠遠高於引用比較,因此 ==
用於值比較更符合直覺。固然若是你確實須要引用比較,Scala 提供了 eq
和 ne
兩個方法:編程
val str1 = new String("Jack") val str2 = new String("Jack") if (str1 eq str2) {}
挑逗指數: 四星json
咱們知道,Scala 一貫以強大的類型推斷聞名於世。不少時候,咱們無須關心 Scala 類型推斷系統的存在,由於不少時候它推斷的結果跟直覺是一致的。 Java 在 2016 年也新增了一份提議JEP 286,計劃爲 Java 10 引入局部變量類型推斷(Local-Variable Type Inference)。利用這個特性,咱們可使用 var 定義變量而無需顯式聲明其類型。不少人認爲這是一項激動人心的特性,可是高興以前咱們要先看看它會爲咱們帶來哪些問題。安全
Java 7 引進了鑽石操做符,使得咱們能夠下降表達式右側的冗餘類型信息,例如:閉包
List<Integer> numbers = new ArrayList<>();
若是引入了 var,則會致使左側的類型丟失,從而致使整個表達式的類型丟失:併發
var numbers = new ArrayList<>();
因此 var 和 鑽石操做符必須二選一,魚與熊掌不可兼得。app
下面是一段檢查用戶是否存在的 Java 代碼:
public boolean userExistsIn(Set<Long> userIds) { var userId = getCurrentUserId(); return userIds.contains(userId); }
請仔細觀察上述代碼,你能一眼看出問題所在嗎? userId 的類型被 var 隱去了,若是 getCurrentUserId() 返回的是 String 類型,上述代碼仍然能夠正常經過編譯,卻無形中埋下了隱患,這個方法將會永遠返回 false, 由於 Set<Long>.contains 方法接受的參數類型是 Object。可能有人會說,就算顯式聲明瞭類型,不也是於事無補嗎?
public boolean userExistsIn(Set<Long> userIds) { String userId = getCurrentUserId(); return userIds.contains(userId); }
Java 的優點在於它的類型可讀性,若是顯式聲明瞭 userId 的類型,雖然仍是能夠正常經過編譯,可是在代碼審查時,這個錯誤將會更容易被發現。 這種類型的錯誤在 Java 中很是容易發生,由於 getCurrentUserId() 方法極可能由於重構而改變了返回類型,而 Java 編譯器卻在關鍵時刻背叛了你,沒有報告任何的編譯錯誤。 雖然這是因爲 Java 的歷史緣由致使的,可是因爲 var 的引入,會致使這個錯誤不斷的蔓延。
很顯然,在 Scala 中,這種低級錯誤是沒法逃過編譯器法眼的:
def userExistsIn(userIds: Set[Long]): Boolean = { val userId = getCurrentUserId() userIds.contains(userId) }
若是 userId 不是 Long 類型,則上面的程序沒法經過編譯。
挑逗指數: 四星
Scala 針對字符做進行了加強,提供了更多的使用操做:
//字符串去重 "aabbcc".distinct // "abc" //取前n個字符,若是n大於字符串長度返回原字符串 "abcd".take(10) // "abcd" //字符串排序 "bcad".sorted // "abcd" //過濾特定字符 "bcad".filter(_ != 'a') // "bcd" //類型轉換 "true".toBoolean "123".toInt "123.0".toDouble
其實你徹底能夠把 String 當作 Seq[Char] 使用,利用 Scala 強大的集合操做,你能夠爲所欲爲地操做字符串。
在 Scala 中,咱們能夠直接書寫原生字符串而不用進行轉義,將字符串內容放入一對三引號內便可:
//包含換行的字符串 val s1= """Welcome here. Type "HELP" for help!""" //包含正則表達式的字符串 val regex = """\d+"""
經過 s 表達式,咱們能夠很方便地在字符串內插值:
val name = "world" val msg = s"hello, ${name}" // hello, world
挑逗指數: 五星
Scala 的集合設計是最容易讓人着迷的地方,就像毒品同樣,一沾上便讓人深陷其中難以自拔。經過 Scala 提供的集合操做,咱們基本上能夠實現 SQL 的所有功能,這也是爲何 Scala 可以在大數據領域獨領風騷的重要緣由之一。
在 Scala 中,咱們能夠這樣初始化一個列表:
val list1 = List(1, 2, 3)
能夠這樣初始化一個 Map:
val map = Map("a" -> 1, "b" -> 2)
全部的集合類型都可以用相似的方式完成初始化,簡潔而富有表達力。
有時方法的返回值可能不止一個,Scala 提供了 Tuple (元組)類型用於臨時存放多個不一樣類型的值,同時可以保證類型安全性。千萬不要認爲使用 Java 的 Array 類型也能夠一樣實現 Tuple 類型的功能,它們之間有着本質的區別。Tuple 會顯式聲明全部元素的各自類型,而不是像 Java Array 那樣,元素類型會被向上轉型爲全部元素的父類型。
咱們能夠這樣初始化一個 Tuple:
val t = ("abc", 123, true) val s: String = t._1 // 取第1個元素 val i: Int = t._2 // 取第2個元素 val b: Boolean = t._3 // 取第3個元素
須要注意的是 Tuple 的元素索引從1開始。
下面的示例代碼是在一個長整型列表中尋找最大值,並返回這個最大值以及它所在的位置:
def max(list: List[Long]): (Long, Int) = list.zipWithIndex.sorted.reverse.head
咱們經過 zipWithIndex 方法獲取每一個元素的索引號,從而將 List[Long] 轉換成了 List[(Long, Int)],而後對其依次進行排序、倒序和取首元素,最終返回最大值及其所在位置。
經過鏈式調用,咱們能夠將關注點放在數據的處理和轉換上,而無需考慮如何存儲和傳遞數據,同時也避免了建立大量無心義的中間變量,大大加強程序的可讀性。其實上面的 max 函數已經演示了鏈式調用。下面咱們演示一下如何使用集合操做實現 SQL 的關聯查詢功能,待實現的 SQL 語句以下:
SELECT p.name, p.company, c.country FROM people p JOIN companies c ON p.companyId = c.id WHERE p.age == 20
上面 SQL 語句實現的功能是關聯查詢 people 和 companies 兩張表,返回年齡爲20歲的全部員工名稱、年齡以及其所在公司名稱。
對應的 Scala 實現代碼以下:
// Entity case class People(name: String, age: Int, companyId: String) case class Company(id: String, name: String) // Entity List val people = List(People("jack", 20, "0")) val companies = List(Company("0", "lightbend")) // 實現關聯查詢 people .filter(p => p.age == 20) .flatMap{ p => companies .filter(c => c.id == p.companyId) .map(c => (p.name, p.age, c.name)) } //結果:List((jack,20,lightbend))
其實使用 for 表達式看起來更加簡潔:
for { p <- people if p.age == 20 c <- companies if p.companyId == c.id } yield (p.name, p.age, c.name)
Scala 的集合操做很是豐富,若是要詳細說明足夠寫一本書了。這裏僅列出一些不那麼經常使用但卻很是好用的操做。
去重:
List(1, 2, 2, 3).distinct // List(1, 2, 3)
交集:
Set(1, 2) & Set(2, 3) // Set(2)
並集:
Set(1, 2) | Set(2, 3) // Set(1, 2, 3)
差集:
Set(1, 2) &~ Set(2, 3) // Set(1)
排列:
List(1, 2, 3).permutations.toList //List(List(1, 2, 3), List(1, 3, 2), List(2, 1, 3), List(2, 3, 1), List(3, 1, 2), List(3, 2, 1))
組合:
List(1, 2, 3).combinations(2).toList // List(List(1, 2), List(1, 3), List(2, 3))
Scala 的並行集合能夠利用多核優點加速計算過程,經過集合上的 par 方法,咱們能夠將原集合轉換成並行集合。並行集合利用分治算法將計算任務分解成不少子任務,而後交給不一樣的線程執行,最後將計算結果進行彙總。下面是一個簡單的示例:
(1 to 10000).par.filter(i => i % 2 == 1).sum
挑逗指數: 五星
Scala 標準庫包含了一個特殊的 Class 叫作 Case Class,專門用於領域層值對象的建模。它的好處是全部的默認行爲都通過了合理的設計,開箱即用。下面咱們使用 Case Class 定義了一個 User 值對象:
case class User(name: String, role: String = "user", addTime: Instant = Instant.now())
僅僅一行代碼便完成了 User 類的定義,請腦補一下 Java 的實現。
咱們爲 role 和 addTime 兩個屬性定義了默認值,因此咱們能夠只使用 name 建立一個 User 實例:
val u = User("jack")
在建立實例時,咱們也能夠命名參數(named parameter)語法改變默認值:
val u = User("jack", role = "admin")
在實際開發中,一個模型類或值對象可能擁有不少屬性,其實不少屬性均可以設置一個合理的默認值。利用默認值和命名參數,咱們能夠很是方便地建立模型類和值對象的實例。 因此在 Scala 中基本上不須要使用工廠模式或構造器模式建立對象,若是對象的建立過程確實很是複雜,則能夠放在伴生對象中建立,例如:
object User { def apply(name: String): User = User(name, "user", Instant.now()) }
在使用伴生對象方法建立實例時能夠省略方法名 apply,例如:
User("jack") // 等價於 User.apply("jack")
在這個例子裏,使用伴生對象方法實例化對象的代碼,與上面使用類構造器的代碼徹底同樣,編譯器會優先選擇伴生對象的 apply 方法。
Case Class 在默認狀況下實例是不可變的,意味着它能夠被任意共享,併發訪問時也無需同步,大大地節省了寶貴的內存空間。而在 Java 中,對象被共享時須要進行深拷貝,不然一個地方的修改會影響到其它地方。例如在 Java 中定義了一個 Role 對象:
public class Role { public String id = ""; public String name = "user"; public Role(String id, String name) { this.id = id; this.name = name; } }
若是在兩個 User 之間共享 Role 實例就會出現問題,就像下面這樣:
u1.role = new Role("user", "user"); u2.role = u1.role;
當咱們修改 u1.role 時,u2 就會受到影響,Java 的解決方式是要麼基於 u1.role 深度克隆一個新對象出來,要麼新建立一個 Role 對象賦值給 u2。
在 Scala 中,既然 Case Class 是不可變的,那麼若是想改變它的值該怎麼辦呢?其實很簡單,利用命名參數能夠很容易拷貝一個新的不可變對象出來:
val u1 = User("jack") val u2 = u1.copy(name = "role", role = "admin")
咱們不須要編寫額外的代碼即可以獲得清晰的調試信息,例如:
val users = List(User("jack"), User("rose")) println(users)
輸出內容以下:
List(User(jack,user,2018-10-20T13:03:16.170Z), User(rose,user,2018-10-20T13:03:16.170Z))
在 Scala 中,默認採用值比較而非引用比較,使用起來更加符合直覺:
User("jack") == User("jack") // true
上面的值比較是開箱即用的,無需重寫 hashCode 和 equals 方法。
挑逗指數: 五星
當你的代碼中存在多個 if 分支而且 if 之間還會有嵌套,那麼代碼的可讀性將會大大下降。而在 Scala 中使用模式匹配能夠很容易地解決這個問題,下面的代碼演示貨幣類型的匹配:
sealed trait Currency case class Dollar(value: Double) extends Currency case class Euro(value: Double) extends Currency val Currency = ... currency match { case Dollar(v) => "$" + v case Euro(v) => "€" + v case _ => "unknown" }
咱們也能夠進行一些複雜的匹配,而且在匹配時能夠增長 if 判斷:
use match { case User("jack", _, _) => ... case User(_, _, addTime) if addTime.isAfter(time) => ... case _ => ... }
利用模式匹配,咱們能夠快速提取特定部分的值並完成變量定義。 咱們能夠將 Tuple 中的值直接賦值給變量:
val tuple = ("jack", "user", Instant.now()) val (name, role, addTime) = tuple // 變量 name, role, addTime 在當前做用域內能夠直接使用
對於 Case Class 也是同樣:
val User(name, role, addTime) = User("jack") // 變量 name, role, addTime 在當前做用域內能夠直接使用
挑逗指數: 五星
在 Scala 中,咱們在編寫併發代碼時只須要關心業務邏輯便可,而不須要關注任務如何執行。咱們能夠經過顯式或隱式方式傳入一個線程池,具體的執行過程由線程池完成。Future 用於啓動一個異步任務而且保存執行結果,咱們能夠用 for 表達式收集多個 Future 的執行結果,從而避免回調地獄:
val f1 = Future{ 1 + 2 } val f2 = Future{ 3 + 4 } for { v1 <- f1 v2 <- f2 }{ println(v1 + v2) // 10 }
使用 Future 開發爬蟲程序將會讓你事半功倍,假如你想同時抓取 100 個頁面數據,一行代碼就能夠了:
Future.sequence(urls.map(url => http.get(url))).foreach{ contents => ...}
Future.sequence 方法用於收集全部 Future 的執行結果,經過 foreach 方法咱們能夠取出收集結果並進行後續處理。
當咱們要實現徹底異步的請求限流時,就須要精細地控制每一個 Future 的執行時機。也就是說咱們須要一個控制Future的開關,沒錯,這個開關就是Promise。每一個Promise實例都會有一個惟一的Future與之相關聯:
val p = Promise[Int]() val f = p.future for (v <- f) { println(v) } // 3秒後纔會執行打印操做 //3秒鐘以後返回3 Thread.sleep(3000) p.success(3)
Java 經過異常機制處理錯誤,可是問題在於 Java 代碼只能捕獲當前線程的異常,而沒法跨線程捕獲異常。而在 Scala 中,咱們能夠經過 Future 捕獲任意線程中發生的異常。
異步任務可能成功也可能失敗,因此咱們須要一種既能夠表示成功,也能夠表示失敗的數據類型,在 Scala 中它就是 Try[T]。Try[T] 有兩個子類型,Success[T]表示成功,Failure[T]表示失敗。就像量子物理學中薛定諤的貓,在異步任務執行以前,你根本沒法預知返回的結果是 Success[T] 仍是 Failure[T],只有當異步任務完成執行之後結果才能肯定下來。
val f = Future{ /*異步任務*/ } // 當異步任務執行完成時 f.value.get match { case Success(v) => // 處理成功狀況 case Failure(t) => // 處理失敗狀況 }
咱們也可讓一個 Future 從錯誤中恢復:
val f = Future{ /*異步任務*/ } for{ result <- f.recover{ case t => /*處理錯誤*/ } } yield { // 處理結果 }
挑逗指數: 四星
Scala 鼓勵聲明式編程,採用聲明式編寫的代碼可讀性更強。與傳統的過程式編程相比,聲明式編程更關注我想作什麼而不是怎麼去作。例如咱們常常要實現分頁操做,每頁返回 10 條數據:
val allUsers = List(User("jack"), User("rose")) val pageList = allUsers .sortBy(u => (u.role, u.name, u.addTime)) // 依次按 role, name, addTime 進行排序 .drop(page * 10) // 跳過以前頁數據 .take(10) // 取當前頁數據,如不足10個則所有返回
你只須要告訴 Scala 要作什麼,好比說先按 role 排序,若是 role 相同則按 name 排序,若是 role 和 name 都相同,再按 addTime 排序。底層具體的排序實現已經封裝好了,開發者無需實現。
挑逗指數: 四星
在 Scala 中,一切都是表達式,包括 if, for, while 等常見的控制結構均是表達式。表達式和語句的不一樣之處在於每一個表達式都有明確的返回值。
val i = if(true){ 1 } else { 0 } // i = 1 val list1 = List(1, 2, 3) val list2 = for(i <- list1) yield { i + 1 }
不一樣的表達式能夠組合在一塊兒造成一個更大的表達式,再結合上模式匹配將會發揮巨大的威力。下面咱們以一個計算加法的解釋器來作說明。
咱們首先定義基本的表達式類型:
abstract class Expr case class Number(num: Int) extends Expr case class PlusExpr(left: Expr, right: Expr) extends Expr
上面定義了兩個表達式類型,Number 表示一個整數表達式, PlusExpr 表示一個加法表達式。
下面咱們基於模式匹配實現表達式的求值運算:
def evalExpr(expr: Expr): Int = { expr match { case Number(n) => n case PlusExpr(left, right) => evalExpr(left) + evalExpr(right) } }
咱們來嘗試針對一個較大的表達式進行求值:
evalExpr(PlusExpr(PlusExpr(Number(1), Number(2)), PlusExpr(Number(3), Number(4)))) // 10
挑逗指數: 五星
若是每當要執行異步任務時,都須要顯式傳入線程池參數,你會不會以爲很煩?Scala 經過隱式參數爲你解除這個煩惱。例如 Future 在建立異步任務時就聲明瞭一個 ExecutionContext 類型的隱式參數,編譯器會自動在當前做用域內尋找合適的 ExecutionContext,若是找不到則會報編譯錯誤:
implicit val ec: ExecutionContext = ??? val f = Future { /*異步任務*/ }
固然咱們也能夠顯式傳遞 ExecutionContext 參數,明確指定使用的線程池:
implicit val ec: ExecutionContext = ??? val f = Future { /*異步任務*/ }(ec)
隱式轉換相比較於隱式參數,使用起來更來靈活。若是 Scala 在編譯時發現了錯誤,在報錯以前,會先對錯誤代碼應用隱式轉換規則,若是在應用規則以後可使得其經過編譯,則表示成功地完成了一次隱式轉換。
當傳入的參數類型和目標類型不匹配時,編譯器會嘗試隱式轉換。利用這個功能,咱們將已有的數據類型無縫對接到三方庫上。例如咱們想在 Scala 項目中使用 MongoDB 的官方 Java 驅動執行數據庫查詢操做,可是查詢接口接受的參數類型是 BsonDocument,因爲使用 BsonDocument 構建查詢比較笨拙,咱們但願可以使用 Scala 的 JSON 庫構建一個查詢對象,而後直接傳遞給官方驅動的查詢接口,而無需改變官方驅動的任何代碼,利用隱式轉換能夠很是輕鬆地實現這個功能:
implicit def toBson(json: JsObject): BsonDocument = ... val json: JsObject = Json.obj("_id" -> "0") jCollection.find(json) // 編譯器會自動調用 toBson(json)
利用隱式轉換,咱們能夠在不改動三方庫代碼的狀況下,將咱們的數據類型與其進行無縫對接。例如咱們經過實現一個隱式轉換,將 Scala 的 JsObject 類型無縫地對接到了 MongoDB 的官方 Java 驅動的查詢接口中,看起就像是 MongoDB 官方驅動真的提供了這個接口同樣。
同時咱們也能夠未來自三方庫的數據類型無縫集成到現有的接口中,也只須要實現一個隱式轉換方法便可。
例如咱們定義了一個美圓貨幣類型 Dollar:
class Dollar(value: Double) { def + (that: Dollar): Dollar = ... def + (that: Int): Dollar = ... }
因而咱們能夠執行以下操做:
val halfDollar = new Dollar(0.5) halfDollar + halfDollar // 1 dollar halfDollar + 0.5 // 1 dollar
可是咱們卻沒法執行像 0.5 + halfDollar 這樣的運算,由於在 Double 類型上沒法找到一個合適的 + 方法。
在 Scala 中,爲了實現上面的運算,咱們只須要實現一個簡單的隱式轉換就能夠了:
implicit def doubleToDollar(d: Double) = new Dollar(d) 0.5 + halfDollar // 等價於 doubleToDollar(0.5) + halfDollar
在平常開發中,咱們一般須要將值對象轉換成 Json 格式以方便數據傳輸。Java 的一般作法是使用反射,可是咱們知道使用反射是要付出代價的,要承受運行時的性能開銷。而 Scala 則能夠在編譯時爲值對象生成隱式的 Json 編解碼對象,這些編解碼對象只不過是普通的函數調用而已,不涉及任何反射操做,在很大程度上提高了系統的運行時性能。
若是你堅持讀到了這裏,我會以爲很是欣慰,很大可能上 Scala 的某些特性已經吸引了你。可是 Scala 的魅力遠不止如此,以上列舉的僅僅是一些最容易抓住你眼球的一些特性。若是你願意推開 Scala 這扇大門,你將會看到一個徹底不同的編程世界。本文歡迎轉載,請註明做者沐風(joymufeng)。
後記
首先感謝你們關注本文,也很是感謝 @dwingo 參與討論。Scala 和 Java 同根同源,而且徹底擁抱現有 Java 生態,在開發中咱們也常用兩種語言混合編程,因此 Scala = Java and More。全文都是圍繞 Scala 的"簡潔性"展開,Scala 在保持簡潔性的同時,提供了很是好的可讀性,能夠參考本文的"聲明式編程"一節。Scala的不少設計都是對 Java 的改良與超越,因此學習 Scala 的過程實際上是一次對 Java 的深度回顧。在這裏向你們推薦一本書《快學Scala 第二版》(高宇翔 譯),做者 Cay S. Horstmann 是一位 Java 語言大師,而且是《Java核心技術》卷1和卷2的做者,全書圍繞Java與Scala展開,行文簡潔通透,讓人醍醐灌頂,尤爲閉包一節甚爲精彩。難能難得的是本書翻譯也很棒,若是你對 Scala 不感冒,僅讀 Java 部分也會受益頗多。
原文連接:https://my.oschina.net/joymufeng/blog/2251038