1 前言
本文的主要目的是为了让Play Framework的初学者快速了解Scala语言,算是一篇Play Framework的入门前传吧。
使用PlayFramework可以极大的提高开发效率,但是需要注意,PlayJava入门很简单,我之前带过一个实习小姑娘,有一点编程经验,但从来没有接触过PlayJava,然而一周入门,一个月独立完成项目。但是PlayScala没那么简单,虽然后者的开发效率更高,但是由于Scala程序员匮乏,PlayScala只适合团队较小(10人以下)并且较稳定的情况下使用。其实有很多人怀疑,Scala到底能提高多少开发效率,这里有一行Scala代码,大家可以先体会一下:
Source.fromFile("D:/f.txt", "UTF-8").getLines().toList.distinct.sortBy(s => (s.charAt(0), s.length)).foreach( println _)
虽然只有一行代码,但是却做了很多事情:以UTF-8编码读取文件所有行 -> 去重 -> 按首字符排序,首字符相同按长度排序 -> 打印结果。各位脑补一下Java的实现。更多的一行代码请查看酷炫的一行代码 - Scala就是这么任性!。下面我们进入正题,先看Scala语言简介。
2 Scala简介
提到编程语言,大家的第一反应通常是面向对象编程(OOP), 然而随着硬件服务器CPU核数和个数越来越多,函数式编程(FP)语言又重新回到了人们的视线。两种编程语言都各有特点,面向对象编程符合人类对世界的认知,更容易理解;函数式编程的语法更接近人类语言,简洁高效。两种语言都让人无法取舍。而Scala将这两种编程语言完美的融合到一起,形成一门更加强大的JVM语言,同时Scala修正了Java很多不合理的设计,新增了更多高级特性,学习Scala的同时也是对Java的一次深度回顾,让你对编程语言的理解更加地深刻。与Java相比,Scala的设计更加一致:
- 一切都是对象
1.toDouble //可以直接调用基本类型上的方法
"1".toInt //将字符串转换成整型
- 一切都是方法
"a" * 3 //等价于: "a".*(3)
2 - 1 //等价于: 2.-(1)
- 一切都是表达式
val i = if(true){ 1 } else { 0 } // i = 1
Scala拥有一套强大的类型推导系统,所以你可以像动态类型语言那样编码,降低代码冗余度,减少无意义击键次数同时,代码也显得更加清晰易懂。
另外Java和Scala对待程序员的态度也很有意思,这里只是开个玩笑,大家别太在意。Java认为他所面对的程序员是一帮小白,容易犯错误,所以想方设法的限制你,避免你犯错;而Scala则认为他所面对的程序员是一帮天才,所以尽可能的向他敞开编程语言宝库,给他更大的自由度去想象和创作。
3 基本语法规则
3.1 变量声明
val用于定义不可变变量,var用于定义可变变量,这里的"可变"指的是引用的可变性。val定义的变量类似于Java的final变量,即变量只能赋一次值:
val msg = "hello" // 等价于:val msg: String = "hello"
var i = 1 // 等价于:var i: Int = 1
i = i + 1
变量后面的类型声明可以省略,每行代码末尾的分号";"也可以省略。
3.2 函数声明
def用于定义函数:
def max(x: Int, y: Int): Int = {
if (x > y) { x } else { y }
}
val maxVal = max(1, 2) // 2
Scala是函数式语言,所以你可以像基本类型那样把函数赋给一个变量:
val max = (x: Int, y: Int) => {
if (x > y) { x } else { y }
}
val maxVal = max(1, 2) // 2
等号"="右边是一个匿名函数,也就是我们常说的Lambda函数,匿名函数由参数和函数体两部分组成,中间用"=>"隔开,这里省略了max变量的类型,因为编译器可以自动推断出来,完整的写法如下:
val max: (Int, Int) => Int = (x: Int, y: Int) => {
if (x > y) { x } else { y }
}
max的类型是(Int, Int) => Int
,即接受两个Int参数,产生一个Int返回值的函数类型。
3.3 class
Scala的class定义和Java很相似:
class Counter {
private var value = 0 //你必须初始化字段
def increment() { value += 1} //方法默认public
def current() = value
}
Scala的源文件中可以定义多个类,并且默认都是public,所以外界都可以看见。class的使用也很简单:
val myCounter = new Counter //或new Counter()
myCounter.increment()
println(myCounter.current) //或myCounter.current()
Scala中如果对象方法或类的构造器没有参数,则括号"()"可以省略。
3.4 object
Scala没有静态方法和静态字段,而是提供了object对象,也就是Java中的单例对象,即全局只有一个实例。
object Accounts {
private var lastNumber = 0
def newUniqueNumber() = { lastNumber += 1; lastNumber }
}
因为Accounts是一个单例对象,可以直接使用而无需初始化:
val uniqueNumber = Accounts.newUniqueNumber
object的另一个用法是作为类的伴生对象, 类似于Java类上的静态方法,只不过Scala将Java类上的静态功能全交给object实现了。object作为伴生对象时必须和类在同一个源文件中定义,并且可以相互访问私有属性。
3.5 apply方法
如果某个对象obj上定义了apply方法,则我们可以这样调用:
obj(arg1, ... , argn)
是的,你猜对了,伴生对象上的apply方法立马就派上用场了,例如List类有一个同名的伴生对象List,那么你可以这样初始化一个列表:
val list = List("a", "b", "c")
想想下面的Java版本,是不是感觉幸福感油然而生:
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
3.6 块表达式
在Scala中一切都是表达式,如果表达式含有多条语句,则使用大括号"{}"括起来,形成一个块表达式,块表达式的最后一条语句的值作为整个块的返回值。
val r = {
val i = 1
val j = 2
i + j
} // r = 3
4 case class和模式匹配
在Scala中接触到新概念不要害怕,了解之后你会发现它帮你解决了很多实际问题,就如我们这里要聊的case class和模式匹配。定义一个case class的代码如下:
case class Currency(value: Double, unit: String)
当你定义了一个case class之后,编译器会自动帮你做如下事情:
- 自动创建伴生对象
- 为该类添加toString,hashCode和euqals方法,用于模式匹配时的结构化比较
- 为该类添加copy方法,用于快速拷贝对象
好了,下面我们来看一下模式匹配的威力:
abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
val amount = Currency(100.0, "EUR")
val amountStr =
amount match {
case Dollar(v) => "$" + v
case Currency(v, u) => "I got " + v + u
case _ => ""
}
在Scala中,类、函数、方法和object可以像变量一样在任何地方定义。
Scala中默认使用的类都是不可变的,所以如果你想改变value的值需要借助copy方法:
val newAmound = amount.copy(value = 1000.0)
Scala中的模式匹配还可以实现更复杂的匹配,详见"Programming in Scala, 3nd Edition"。如果说Java中的switch是一把手枪,那么Scala中的模式匹配是一架当之无愧的战头机。
5 map和flatMap
可能有很多人就是因为这两个方法才迷恋上Scala的。map和flatMap是两个高阶函数,所谓高阶函数就是接受函数作为参数的函数。这两个方法各自接受一个一元函数(即只有一个参数的函数,类型为:(A) => B
),利用这个一元函数,你可以对数据流中的每一个元素进行一些操作或转换,最终得到一个全新的数据流。
map方法接受的一元函数类型为:(A) => B
:
List(1, 2, 3).map((i: Int) => { i + 1 }) // List(2, 3, 4)
也可以简写如下两种形式:
List(1, 2, 3).map(i => i + 1 )
List(1, 2, 3).map(_ + 1 )
你可以把第2种形式中的下划线理解成每个元素的占位符,其实这只是编译器的语法糖,编译后的结果和前两种写法相同。使用这个语法糖的前提是下划线"_"在函数体内只能出现一次。
在上面的例子里,map方法接受的一元函数类型是:(Int) => Int
,元素的类型没有发生改变,我们可以尝试改变元素类型:
List(1, 2, 3).map(i => i.toString * i) // List(1, 22, 333)
这次传入的一元函数类型是: (Int) => String
,将原List从List[Int]
类型转换成了List[String]
类型,完成一次数据流类型转换。
flatMap方法接受的一元函数类型为:(A) => List[B]
,我们发现该一元函数返回的类型也是一个List,flatMap方法会自动将由每个元素A转换成的小List[B]展平成一个大的List[B],这也是flatMap中的"flat"所要表达的意思:
List(1, 2, 3).flatMap(i => List(i, i)) // List(1, 1, 2, 2, 3, 3)
这里我们只在List上演示了map和flatMap的基本用法,Scala中所有的容器类型(例如Option, Either, Future, Set, ...)都内置了这两个方法。除了map和flatMap,Scala的容器类型上还有很多类似的方法,例如filter, find, sortBy等等,详见"Programming in Scala, 3nd Edition"。
6 常用类介绍
6.1 String
在Scala中,String更加方便好用:
//原始字符串一对三引号`"""`括起来,可包含多行字符串,内容不需要转义
"""Welcome here.
Type "HELP" for help!"""
//类型转换
"100.0".toDouble
//判断字符串相等直接用"==",而不需要使用equals方法
val s1 = new String("a")
s1 == "a" // true
//字符串去重
"aabbcc".distinct // "abc"
//取前n个字符,如果n大于字符串长度返回原字符串
"abcd".take(10) // "abcd"
//字符串排序
"bcad".sorted // "abcd"
//过滤特定字符
"bcad".filter(_ != 'a') // "bcd"
//字符串插值, 以s开头的字符串内部可以直接插入变量,方便字符串构造
val i = 100
s"i=${i}" // "i=100"
Scala中没有受检异常(checked exception),所以你没有必要声明受检异常,如果真的发生异常,则会在运行时抛出。
6.2 Option
Scala用Option类型表示一个值是否存在,用来避免Java的NullPointerException。它有两个子类:Some和None。Some类型表示值存在,None类型则表示值不存在。
常用操作:
val opt: Option[String] = Some("hello")
//判断是否为None
opt.isEmpty // false
//如果为None,则返回默认值"default",否则返回opt持有的值
opt.getOrElse("default")
//如果为None则返回"DEFAULT",否则将字符转为大写
opt.fold("DEFAULT"){ value => value.toUpperCase } // "HELLO"
//功能同上
opt match {
case Some(v) => v.toUpperCase
case None => "DEFAULT"
}
6.3 List
在Scala中,List要么是Nil(空列表),要么就是由head和tail组成的递归结构。 head是首元素,tail是剩下的List。所以你可以这样构建List:
val list = 1 :: Nil // 等价于:val list = List(1)
连续的两个冒号"::"就像是胶水,将List的head和tail粘在一起。
常用操作:
val list = List(1, 3, 2)
//获取第1个元素
list.headOption.getOrElse(0) // 1
//查找
list.find(_ % 2 == 0).getOrElse(0) // 2
//过滤
list.filter(_ % 2 == 1) // List(1, 3)
//排序
list.sorted // List(1, 2, 3)
//最小值/最大值/求和
list.min // 1
list.max // 3
list.sum // 6
//转化成字符串
list.mkString(",") // "1, 3, 2"
Scala提供的List基本可以实现SQL查询的所有功能,这也是Spark为什么基于Scala开发的原因。更多功能请参考官方文档。
在Scala中默认的集合类例如List,Set,Map,Tuple等都是不可变的,所以调用其修改方法会返回一个新的实例。如果要使用可变集合,请使用
scala.collection.mutable
包下相应的类。不可变类型在编写并发代码时很有用。
6.4 Tuple
Tuple(元组)Tuple可以容纳不同类型的元素,最简单的形态是二元组,即由两个元素构成的Tuple, 可以使用_1, _2等方法访问其元素:
val t = ("a", 1) // 等价于:val t: Tuple2[String, Int] = ("a", 1)
t._1 // "a"
t._2 // 1
也可以使用模式匹配利用Tuple同时初始化一组变量:
val t = ("a", 1)
val (v1, v2) = t
v1 // "a"
v2 // 1
6.5 Map
Map其实是二元组的集合:
val map = Map("a" -> 1, "b" -> 2)
"->"其实是String类型上的方法,返回一个二元组:
"a" -> 1 //等价于: ("a", 1)
所以你也可以这样构建Map:
val map = Map(("a", 1), ("b", 2))
常用操作:
val map = Map("a" -> 1, "b" -> 2)
//读取
map("a") // 1
//写入或添加键值
map("a") = 0
//删除键值
map - "a" // Map(b -> 2)
7 控制结构
7.1 if
if语句同样是表达式,拥有返回值:
val i = 1
val r = if(i > 0){ 1 } else { 0 } // r = 1
7.2 for
Scala中for语句功能比Java要丰富很多,你可以使用for遍历一个List:
val list = List(1, 2, 3)
for(i <- list){
println(i)
}
你也可以使用模式匹配遍历一个Map:
val map = Map(("a", 1), ("b", 2))
for((k, v) <- map){
println(k + ": " + v)
}
如果循环体以yield开始,for语句会返回一个新的集合:
val newList1 = for(i <- List(1, 2, 3)) yield i * 2 // List(2, 4, 6)
val newList2 =
for{
i <- List(1, 2)
j <- List(3, 4)
} yield i + j //List(4, 5, 5, 6)
如果有多个集合需要遍历,则for语句后面的圆括号"()"要换成大括号"{}"。
8 Future和Promise
Future和Promise是Scala提供的最吸引人的特性之一,借助Future和Promise你可以轻松地编写完全异步非阻塞的代码,这在多处理器时代显得格外重要。
8.1 Future
Future用于获取异步任务的返回结果。Future有两种状态:完成(completed)和未完成(not completed)。处于完成状态的Future可能包含两种情况的信息,一种是异步任务执行成功了,Future中包含异步任务执行成功的返回结果;另一种是异步任务执行失败了,Future中包含了相应的Exception信息。Future的独特之处在于它的值只能被写入一次,之后就会变为一个不可变值,其中包含成功或失败信息。你可以在Future上注册一个回调函数,以便在任务执行完成后得到通知:
import scala.concurrent.ExecutionContext.Implicits.global
val f = Future{ 1 + 2 }
f.onComplete{ t =>
t match{
case Success(v) => println("success: " + v)
case Failure(t) => println("failed: " + t.getMessage)
}
}
//等待任务结束
Await.ready(f, 10 seconds)
onComplete方法接受一个一元函数,类型为:Try[T] => U
。Try类型和Option类型很像,也有两个子类Success
和Failure
,前者表示任务执行成功,后者表示任务执行失败。
第1行import语句导入了一个隐式的ExecutionContext,你可以把它理解成是一个线程池,Future类在需要时会自动使用其上的线程。在Scala中你不需要直接和线程打交道。
由于Future也是一个容器类,所以可以使用for语句取回它的值:
val f = Future{ 1 + 2 }
for(v <- f) {
println(v) // 3
}
也可以使用map方法对任务结果进行转换:
val f1 = Future{ 1 + 2 }
val f2 = f1.map(v => v % 2)
for(v <- f2) {
println(v) // 1
}
利用for语句可以等待多个Future的返回结果:
val f1 = Future{ 1 + 2 }
val f2 = Future{ 3 + 4 }
for{
v1 <- f1
v2 <- f2
} {
println(v1 + v2) // 10
}
结合yield可以返回一个新的Future:
val f1 = Future{ 1 + 2 }
val f2 = Future{ 3 + 4 }
val f3 =
for{
v1 <- f1
v2 <- f2
} yield {
v1 + v2
}
8.2 Promise
有时我们需要精细地控制Future的完成时机和返回结果,也就是说我们需要一个控制Future的开关,没错,这个开关就是Promise。每个Promise实例都会有一个唯一的Future与之相关联:
val p = Promise[Int]()
val f = p.future
for(v <- f) { println(v) }
//3秒钟之后返回3
Thread.sleep(3000)
p.success(3)
//等待任务结束
Await.ready(f, 10 seconds)
9 小结
Scala在刚入门的时候确实有点难度,各种奇怪的语法、符号漫天飞,看的云里雾里。但是在你入门之后会发现,这些奇怪的地方其实是合理的,是一种有意的设计。例如允许方法名包含特殊符号,你可以写出下面的代码:
"a" * 3 // "aaa"
val map = Map("a" -> 1, "b" -> 2)
"*"和"->"其实是字符串上的两个方法,允许符号作为方法名使得代码直观易懂。由于Scala赋予程序员对代码很高的控制力,如果滥用就会导致天书般的代码,这需要团队内部进行协调,控制代码的复杂度。Scala之父Martin Odersky也曾经表示会在2016简化Scala语言,降低初学者的门槛。到时会有更多的人加入这个社区,一起分享编程的乐趣。
10 参考
- "Programming in Scala, 3nd Edition"
- "快学Scala"
11 附录
11.1 开发工具推荐
IntelliJ IDEA + Scala插件
11.2 转载声明
转载请注明作者joymufeng