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)

一切都是表达式

vali=if(true){1}else{0}//i=1

Scala拥有一套强大的类型推导系统,所以你可以像动态类型语言那样编码,降低代码冗余度,减少无意义击键次数同时,代码也显得更加清晰易懂。
另外Java和Scala对待程序员的态度也很有意思,这里只是开个玩笑,大家别太在意。Java认为他所面对的程序员是一帮小白,容易犯错误,所以想方设法的限制你,避免你犯错;而Scala则认为他所面对的程序员是一帮天才,所以尽可能的向他敞开编程语言宝库,给他更大的自由度去想象和创作。

3 基本语法规则3.1 变量声明

val用于定义不可变变量,var用于定义可变变量,这里的"可变"指的是引用的可变性。val定义的变量类似于Java的final变量,即变量只能赋一次值:

valmsg="hello"//等价于:valmsg:String="hello"vari=1//等价于:vari:Int=1i=i+1

变量后面的类型声明可以省略,每行代码末尾的分号";"也可以省略。

3.2 函数声明

def用于定义函数:

defmax(x:Int,y:Int):Int={if(x>y){x}else{y}}valmaxVal=max(1,2)//2

Scala是函数式语言,所以你可以像基本类型那样把函数赋给一个变量:

valmax=(x:Int,y:Int)=>{if(x>y){x}else{y}}valmaxVal=max(1,2)//2

等号"="右边是一个匿名函数,也就是我们常说的Lambda函数,匿名函数由参数和函数体两部分组成,中间用"=>"隔开,这里省略了max变量的类型,因为编译器可以自动推断出来,完整的写法如下:

valmax:(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很相似:

classCounter{privatevarvalue=0//你必须初始化字段defincrement(){value+=1}//方法默认publicdefcurrent()=value}

Scala的源文件中可以定义多个类,并且默认都是public,所以外界都可以看见。class的使用也很简单:

valmyCounter=newCounter//或newCounter()myCounter.increment()println(myCounter.current)//或myCounter.current()

Scala中如果对象方法或类的构造器没有参数,则括号"()"可以省略。

3.4 object

Scala没有静态方法和静态字段,而是提供了object对象,也就是Java中的单例对象,即全局只有一个实例。

objectAccounts{privatevarlastNumber=0defnewUniqueNumber()={lastNumber+=1;lastNumber}}

因为Accounts是一个单例对象,可以直接使用而无需初始化:

valuniqueNumber=Accounts.newUniqueNumber

object的另一个用法是作为类的伴生对象, 类似于Java类上的静态方法,只不过Scala将Java类上的静态功能全交给object实现了。object作为伴生对象时必须和类在同一个源文件中定义,并且可以相互访问私有属性。

3.5 apply方法

如果某个对象obj上定义了apply方法,则我们可以这样调用:

obj(arg1,...,argn)

是的,你猜对了,伴生对象上的apply方法立马就派上用场了,例如List类有一个同名的伴生对象List,那么你可以这样初始化一个列表:

vallist=List("a","b","c")

想想下面的Java版本,是不是感觉幸福感油然而生:

List<String>list=newArrayList<String>();list.add("a");list.add("b");list.add("c");3.6 块表达式

在Scala中一切都是表达式,如果表达式含有多条语句,则使用大括号"{}"括起来,形成一个块表达式,块表达式的最后一条语句的值作为整个块的返回值。

valr={vali=1valj=2i+j}//r=34 case class和模式匹配

在Scala中接触到新概念不要害怕,了解之后你会发现它帮你解决了很多实际问题,就如我们这里要聊的case class和模式匹配。定义一个case class的代码如下:

caseclassCurrency(value:Double,unit:String)

当你定义了一个case class之后,编译器会自动帮你做如下事情:

自动创建伴生对象

为该类添加toString,hashCode和euqals方法,用于模式匹配时的结构化比较

为该类添加copy方法,用于快速拷贝对象

好了,下面我们来看一下模式匹配的威力:

abstractclassAmountcaseclassDollar(value:Double)extendsAmountcaseclassCurrency(value:Double,unit:String)extendsAmountvalamount=Currency(100.0,"EUR")valamountStr=amountmatch{caseDollar(v)=>"$"+vcaseCurrency(v,u)=>"Igot"+v+ucase_=>""}

在Scala中,类、函数、方法和object可以像变量一样在任何地方定义。

Scala中默认使用的类都是不可变的,所以如果你想改变value的值需要借助copy方法:

valnewAmound=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更加方便好用:

//原始字符串一对三引号`"""`括起来,可包含多行字符串,内容不需要转义"""Welcomehere.Type"HELP"forhelp!"""//类型转换"100.0".toDouble//判断字符串相等直接用"==",而不需要使用equals方法vals1=newString("a")s1=="a"//true//字符串去重"aabbcc".distinct//"abc"//取前n个字符,如果n大于字符串长度返回原字符串"abcd".take(10)//"abcd"//字符串排序"bcad".sorted//"abcd"//过滤特定字符"bcad".filter(_!='a')//"bcd"//字符串插值,以s开头的字符串内部可以直接插入变量,方便字符串构造vali=100s"i=${i}"//"i=100"

Scala中没有受检异常(checked exception),所以你没有必要声明受检异常,如果真的发生异常,则会在运行时抛出。

6.2 Option

Scala用Option类型表示一个值是否存在,用来避免Java的NullPointerException。它有两个子类:Some和None。Some类型表示值存在,None类型则表示值不存在。
常用操作:

valopt:Option[String]=Some("hello")//判断是否为Noneopt.isEmpty//false//如果为None,则返回默认值"default",否则返回opt持有的值opt.getOrElse("default")//如果为None则返回"DEFAULT",否则将字符转为大写opt.fold("DEFAULT"){value=>value.toUpperCase}//"HELLO"//功能同上optmatch{caseSome(v)=>v.toUpperCasecaseNone=>"DEFAULT"}6.3 List

在Scala中,List要么是Nil(空列表),要么就是由head和tail组成的递归结构。 head是首元素,tail是剩下的List。所以你可以这样构建List:

vallist=1::Nil//等价于:vallist=List(1)

连续的两个冒号"::"就像是胶水,将List的head和tail粘在一起。
常用操作:

vallist=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//1list.max//3list.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等方法访问其元素:

valt=("a",1)//等价于:valt:Tuple2[String,Int]=("a",1)t._1//"a"t._2//1

也可以使用模式匹配利用Tuple同时初始化一组变量:

valt=("a",1)val(v1,v2)=tv1//"a"v2//16.5 Map

Map其实是二元组的集合:

valmap=Map("a"->1,"b"->2)

"->"其实是String类型上的方法,返回一个二元组:

"a"->1//等价于:("a",1)

所以你也可以这样构建Map:

valmap=Map(("a",1),("b",2))

常用操作:

valmap=Map("a"->1,"b"->2)//读取map("a")//1//写入或添加键值map("a")=0//删除键值map-"a"//Map(b->2)7 控制结构7.1 if

if语句同样是表达式,拥有返回值:

vali=1valr=if(i>0){1}else{0}//r=17.2 for

Scala中for语句功能比Java要丰富很多,你可以使用for遍历一个List:

vallist=List(1,2,3)for(i<-list){println(i)}

你也可以使用模式匹配遍历一个Map:

valmap=Map(("a",1),("b",2))for((k,v)<-map){println(k+":"+v)}

如果循环体以yield开始,for语句会返回一个新的集合:

valnewList1=for(i<-List(1,2,3))yieldi*2//List(2,4,6)valnewList2=for{i<-List(1,2)j<-List(3,4)}yieldi+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上注册一个回调函数,以便在任务执行完成后得到通知:

importscala.concurrent.ExecutionContext.Implicits.globalvalf=Future{1+2}f.onComplete{t=>tmatch{caseSuccess(v)=>println("success:"+v)caseFailure(t)=>println("failed:"+t.getMessage)}}//等待任务结束Await.ready(f,10seconds)

onComplete方法接受一个一元函数,类型为:Try[T] => U。Try类型和Option类型很像,也有两个子类SuccessFailure,前者表示任务执行成功,后者表示任务执行失败。

第1行import语句导入了一个隐式的ExecutionContext,你可以把它理解成是一个线程池,Future类在需要时会自动使用其上的线程。在Scala中你不需要直接和线程打交道。

由于Future也是一个容器类,所以可以使用for语句取回它的值:

valf=Future{1+2}for(v<-f){println(v)//3}

也可以使用map方法对任务结果进行转换:

valf1=Future{1+2}valf2=f1.map(v=>v%2)for(v<-f2){println(v)//1}

利用for语句可以等待多个Future的返回结果:

valf1=Future{1+2}valf2=Future{3+4}for{v1<-f1v2<-f2}{println(v1+v2)//10}

结合yield可以返回一个新的Future:

valf1=Future{1+2}valf2=Future{3+4}valf3=for{v1<-f1v2<-f2}yield{v1+v2}8.2 Promise

有时我们需要精细地控制Future的完成时机和返回结果,也就是说我们需要一个控制Future的开关,没错,这个开关就是Promise。每个Promise实例都会有一个唯一的Future与之相关联:

valp=Promise[Int]()valf=p.futurefor(v<-f){println(v)}//3秒钟之后返回3Thread.sleep(3000)p.success(3)//等待任务结束Await.ready(f,10seconds)9 小结

Scala在刚入门的时候确实有点难度,各种奇怪的语法、符号漫天飞,看的云里雾里。但是在你入门之后会发现,这些奇怪的地方其实是合理的,是一种有意的设计。例如允许方法名包含特殊符号,你可以写出下面的代码:

"a"*3//"aaa"valmap=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