本文是对Scala语言的基本语法的一个学习总结,共包括如下章节:
- 基本元素
- 结构化语句
- 数据类型
- 小结
参考资料:
1、如果需要对scala的基本概念和开发环境搭建进行了解,可参考文章《Scala学习笔记(1)-快速起步》。
一、基本元素
(一)数据类型
scala是一种强类型语言,所有数据都是有类型的。scala的数据类型可分为两种:
1、一种是scala预定义的一些数据类型,比如预定义的值类型(如Int,Char等)、预定义的引用类型(如字符串类型String)。
2、另一种是自定义的数据类型,如自定义类。
(二)基础数据类型
scala中有如下基础数据类型,与Java中是一致的。如下表所列:
类型名 | 含义 | 类型名 | 含义 |
---|---|---|---|
Byte | 8位有符号整数 | Float | 32位单精度浮点数 |
Short | 16位有符号整数 | Double | 32位双精度浮点数 |
Int | 32位有符号整数 | Char | 16位无符号数 |
Long | 64位有符号整数 | Boolean | 布尔型,true或false |
需要注意的是,scala并没有基本类型,scala是一种纯面向对象的编程语言。它的每一个基础类型都是一个类。当把一个scala应用程序编译成java字节码的时候,编译器会自动把scala基础类型转变成java基本类型,这样有助于提高应用程序的性能。
(三)变量定义
scala有两种类型的变量:可变和不可变的。
可变变量使用关键字var声明,可以在创建之后重新赋值,如:
var x = 10
x =20
不可变变量使用关键字val声明,在初始化之后不可以重新赋值了,如:
val y = 10
y =20 //这个语句会报编译器错误
scala语言的变量定义有下特点:
1、需要用关键字var 或 val来定义
2、变量定义时就必须进行初始化赋值
3、变量定义时可以省去类型名,会根据赋值进行类型推断,如下面两个语句是等价的:
var x: Int= 10
var x =10
如果要声明类型,类型名在变量名后面,之间以冒号分隔。显然,不加类型代码更简洁。
(四)字面量
scala中各数据类型的字面量与java中的一致。
(五)表达式
表达式是可计算的语句,每个表达式都有一个返回值。
(六)语句块
利用{}将多个语句组成一个语句块,语句块也是有返回值的,块中最后一个表达式的结果也是整个块的结果。如下面是合法的代码:
object Hello {
def main(args: Array[String]) {
var x=
{
val x = 1 + 1
x + 1
}
println(x)
}
}
(七)类
scala是一种纯面向对象的编程语言,类是面向对象编程的基本单元,所有的代码都包含在类中。scala的类和Java中的类基本相似,但也有一些自己的特点。下面是一个简单的类的定义:
class Person{
var name:String="tom"
def show()={
println(name)
}
}
上面代码中定义了一个Person类,类中有一个成员变量name和一个成员方法show,可以看出,和java中的类定义类似。
scala中的类是一种用户自定义的数据类型。关于scala的类及面向对象的编程的更详细的信息我们在后面的文章中介绍。
(八)函数和方法
函数和方法这两个名词在很多语言中都会提到,通常来说,在类中定义的作为类的成员的函数称为方法。比如在c语言中,没有类的概念,所以没有方法的概念,所以全部称为函数。而在c++中,函数既可以独立在类的外部,也可以定义在类的内部作为类的成员,所以在类的外部定义时我们称为函数,在类的内部作为成员定义时我们称为方法。
函数还是方法都是完成一个特定功能的代码集合,无论是函数还是方法,都有几个共同的特性:
1、输入参数列表,每个函数或方法都可以有0个或多个参数
2、返回值,函数或方法都会有一个返回值
3、代码,函数或方法通过其包含的代码来完成其设定的功能。只有被调用后,代码才会被执行。
在scala中,我们使用def关键字定义方法(如上面Person类中的show方法),scala中的方法的语法和java中的方法类似。关于scala中方法的详细介绍,参见《Scala学习笔记(3)-面向对象编程上篇》;关于scala函数的含义,以及函数与方法的区别,参见《Scala学习笔记(5)-函数式编程》中的介绍。
二、结构化语句
(一)if条件语句
scala的if条件语句语法与java的完全一致,本文不再介绍。
需要说明一点的是,在scala中,任何语句都是一个表达式,表达式都是有值的。if语句也不例外,可以将if作为一个表达式赋值给变量。如下面的例子:
scala> val re = if(3>1) println("A")
A
re: Unit = ()
scala> val re = if(3>5) println("A")
re: Unit = ()
scala> val re = if(3>5) 10 else 5
re: Int = 5
if语句作为一个表达式,其返回值实际是if语句中最后一个被执行语句的返回值。上面代码中,第一个if语句最后执行的是 println("A") 语句,因为println方法的返回值是Unit类型,Unit类型具有唯一实例(),后面会详细介绍,这里理解成java中的void即可。第二个if语句没有满足条件的语句被执行,返回值也是Unit类型,值为()。第三个if语句,最后执行的语句就是个整数表达式5,所以返回的类型是Int型,值为5。
(二)循环语句
1、while循环
scala语言提供了和java中一样用法的while循环和do while循环。下面看一个使用while循环的简单例子:
scala> var i=1
i: Int = 1
scala> while(i<5){
| print(i+",")
| i=i+1
| }
1,2,3,4,
scala>
2、for循环
scala语言提供了for循环语句,和c/c++/java不同的是,没有for(初始化变量;条件判断;更新变量)这样的循环语法,而是支持对集合的遍历来实现循环(类似Java中for语句对Java的遍历)。for循环的语法格式如下:
for(元素变量 <- 集合对象){
.....循环代码
}
如果循环体代码只有一条语句,则{}可省略。
我们来看一个简单例子:
scala> var list = List("hello ","world ","good")
list: List[String] = List("hello ", "world ", good)
scala> for(item <- list){
| print(item)
| }
hello world good
scala>
上面代码中的list变量是一个List类型的实例,List是scala中的列表集合,和Java中的List接口类似。
3、有过滤条件的for循环,语法格式如:
for(元素变量 <- 集合对象 if条件判断1; if条件判断2.....){
//所有条件都满足才执行循环中的语句
}
下面看一个具体的例子,如下:
scala> var list = List(1,5,2,7,4)
list: List[Int] = List(1, 5, 2, 7, 4)
scala> for(item <- list if item>2;if item<7) print(item)
54
scala>
说明,条件不满足并不是结束(或跳出)整个for循环,而是不执行本次循环体中的语句。
关于如何跳出循环,下面会进行介绍。
同if语句一样,循环语句也可以作为一个表达式,也有返回值,可以赋值给变量,只不过scala的循环语句,无论是while还是for,不管循环体中是什么代码,当作表达式时,返回值的类型总是为Unit。如下面例子:
scala> val re = for(i<-List(2,3)) 5
re: Unit = ()
scala> val re = for(i<-List(2,3)) print(i)
23re: Unit = ()
可以看出,无论循环体中执行的是一个表达式,还是一个返回值为Unit的方法调用,for循环作为一个表达式返回值都是Unit类型。
(三)跳出循环
前面我们介绍了scala中的循环语句,但一直没有提到如何跳出循环,在java中,提供了break和continue的关键字用于跳出循环和提前结束本次循环,但scala中没有这两个关键字。在scala中,可以通过其它方法来结束循环。
实现contine关键字的功能比较简单,只需通过if语句来实现。我们看一段Java代码使用contine关键字的例子:
int i=10;
while(i>0){
i=i-1;
if(i%2==0){
continue;
}
System.out.println(i);
}
上面代码不用continue关键字,只需改变下if语句,如下面代码是scala中的实现:
var i=10
while(i>0){
i=i-1
if(i%2!=0){
println(i)
}
}
不过想替代break关键字,尤其是对于for循环,就相对麻烦一些了。下面我们来看看怎么处理。
方法一:通过变量来控制
对于while循环,因为天生其条件就是布尔表达式,只需在循环体中根据情况修改变量的值,让条件表达式为false,就可以结束循环。对于for循环,也可以加上一个条件变量来实现,如下面例子:
var list = List(1,5,-1,7,4)
var flag=true
for(item <- list if flag){
if(item<0)
flag=false
else
print(item)
}
显然对于for循环,这不是一种好的方式,因为效果上能实现类似跳出循环,但实际是计算flag值为false后,对于集合中的剩余元素,每个元素还是得判断一下,只是因为flag的值一直为false了,循环体不会被执行。
方法二:使用scala.util.control.Breaks类
Breaks类中的breakable方法可以跟一代码块,在代码块中调用Breaks类的break方法可以跳出这个代码块,如下面例子:
import scala.util.control.Breaks._
println("begin")
breakable{
println("a1")
break
println("a2")
}
println("end")
上面代码首先通过import语句将Breaks类引入。breakable方法后跟着的是代码块,实际这是breakable方法的参数,如果我们查看breakable方法的源代码,会发现breakable方法的参数就是一个不带任何参数的函数类型。而一个代码块就可以看作是不带参数的函数,所以可以传递给breakable方法。在该代码块中,我们调用了break方法。执行上述代码,我们会发现break语句后面的 println("a2") 语句没有被执行。
有了Breaks这个类的功能,我们只需将循环代码放入到breakable方法的参数代码块中,然后在循环体中根据条件调用Breaks类的break方法,这样就可以跳出整个代码块,也就跳出了循环。如上面for循环例子可这样实现:
import scala.util.control.Breaks._
var list = List(1,5,-1,7,4)
breakable{
for(item <- list){
if(item<0) break
print(item)
}
}
显然,我们也可以很容易的实现嵌套循环中从内循环中跳到最外部,如下面例子:
import scala.util.control.Breaks._
var list = List(1,5,-1,7,4)
breakable{
for(i <- List(1,2,3))
for(item <- list){
if(item<0) break
print(item)
}
}
可以看出,使用Breaks类提供的方法来跳出循环,在简单场景下和java中使用break关键字有点类似,但Breaks类的功能更加强大和通用。
方法三:使用内嵌方法和return语句
我们可以将循环语句放入到一个方法中,然后当条件满足时,使用return语句,这样方法都结束了,自然循环就结束了。如下面例子:
def func(){
println("begin")
def test(){
for(item <- List(1,5,-1,7,4) ){
if(item<0)
return
println(item)
}
}
test()
println("end")
}
func()
上面代码在方法func的内部定义了一个test方法,在test方法的for循环中使用return语句提前结束方法,也就是提前结束了循环。这种方式虽然可以实现跳出循环,但要显示定义一个方法,显然降低了代码的整洁度。
三、数据类型
(一)概述
scala是一种纯面向对象的编程语言,所有的数据类型都是类,所有的值都是对象。scala语言中所有的类都继承自一个共同的超类Any,是scala类层级的根节点,在其下面有两个子类:AnyVal和AnyRef。
其中AnyVal是Scala中所有值类型的超类,scala中有9种预定义的值类型,这9种预定义的值类型分别是Double,Float,Long,Int,Short,Byte,Unit,Char和Boolean。值类型的实例必须都有值。
AnyRef 代表引用类型,全部的非值类型都被定义为引用类型。在Scala中预定义的一些类型,如字符串类型String,集合类型List,Set等,以及用户自定义的每个类型都是 AnyRef 的子类型。AnyRef 相当于java中的java.lang.Object类。
下图显示了scala的类型层次关系。
在上图的scala类型层次图中我们看到最下面还有Null和Nothing两个类型,后面会专门介绍。
(二)基础类型的隐式(implict)转换
上面提到,AnyVal类型的子类中,有9种预定义的值类型分别是Double,Float,Long,Int,Short,Byte,Char,Boolean和Unit。除Boolean和Unit类型外,其它的值型之间可以实现隐式的转换。它们的转换关系如下:
也就是按照上图箭头的方向,比如Byte类型的数据可以直接赋值给Short及之后的类型的变量。因为越是往后,类型的存储范围越大,高范围的可与容纳低范围的,反之不行。如:
scala> var b:Byte=100
b: Byte = 100
scala> var s:Short=50
s: Short = 50
scala> s=b
s: Short = 100
scala> b=s
:13: error: type mismatch;
found : Short
required: Byte
b=s
从上面例子可以看出,Byte类型的变量赋值给Short类型的没问题,反之会报错。Char类型虽然是保存的字符,可以赋值给Int类型及后面的类型,赋值时会自动按照ASCII码来转换,但反之不行。不过可以将字面量的整数赋值给Char类型,会自动转换为字符。如:
scala> var c:Char='a'
c: Char = a
scala> c=98
c: Char = b
scala> var a:Int = c
a: Int = 98
scala> c=a
:13: error: type mismatch;
found : Int
required: Char
c=a
^
(三)Null和Nothing
前面的scala类型层次图中我们看到最下面还有Null和Nothing两个类型。
其中Null 是所有引用类型的子类型(也就是说是AnyRef 的任何子类型)。它具有唯一的实例null(是个关键字)。null 主要是为了和其它JVM语言的交互而提供的,并且应该永远不要在Scala代码中使用它。
Nothing 是所有类型的子类型(包括Null类型),也叫做底部类型。但是Nothing类却不存在任何实例。Nothing的主要用处体现在两个地方:
1、在定义空列表时使用,如:
scala> var list = List()
list: List[Nothing] = List()
2、作为方法不正常返回的返回类型使用,如:
def error(msg:String):Nothing={
throw new RuntimeException(msg)
}
def divide(x: Int, y: Int): Int ={
if (y != 0)
x / y
else
error("can not divide by zero")
}
(四)Unit类型
9种值类型中还有一个Unit类型,这个在java中没有对应的类型,它是scala中一种特殊的类型,有唯一的实例() ,代表没有值,类似c或Java中的void。通常用作不返回任何结果的方法的结果类型。如:
scala> val re = println("hello")
hello
re: Unit = ()
scala> def test() = {}
test: ()Unit
在上面的第一个例子中,println函数只是输出信息,不返回任何结果。这时调用时返回的就是()值。第二个例子,定义了一个空函数,显示函数的返回类型就是Unit类型。
当然我们也可以显示的定义一个方法的返回值为Unit类型,如:
def test(info:String):Unit={
println(info)
}
(五)Option类型
大多数语言都有一个特殊的关键字或者对象来表示一个对象引用的是“无”,在Java中,它是null。在Java 里,null 是一个关键字,不是一个对象,所以对它调用任何方法都是非法的。但是这对语言设计者来说是一件令人疑惑的选择。为什么要在程序员希望返回一个对象的时候返回一个关键字呢?
在前面部分已经提到,scala中的Null类型,有唯一的实例null(是个关键字),实际上scala提供null主要是为了和其它JVM语言的交互而提供的,并且应该永远不要在Scala代码中使用它。
在scala中,我们应该使用Option类型。Scala中的Option(选项)类型用来表示一个值是可选的(有值或无值)。它是一个泛型类型,Option[T] 是一个类型为 T 的可选值的容器: 如果值存在, Option[T] 的值就是Some[T] ,如果不存在, Option[T] 的值就是 None 。
Scala程序使用Option非常频繁,在Java中使用null来表示空值,代码中很多地方都要添加null关键字检测,不然很容易出现NullPointException。因此Java程序需要关心那些变量可能是null,而这些变量出现null的可能性很低,但一但出现,很难查出为什么出现NullPointerException。 Scala的Option类型可以避免这种情况,因此Scala应用推荐使用Option类型来代表一些可选值。比如如果一个函数的返回值可能不会引用任何值,则返回类型可以定义为Option类型,这样调用者一眼就可以看出这种类型的值可能为None。
比如scala提供的数据结构map,其get方法(根据key获取value)返回的就是一个Option类型。这么设计的原因是因为key有可能不存在。我们看一个例子:
scala> val myMap: Map[String, String] = Map("key1" -> "hello")
myMap: Map[String,String] = Map(key1 -> hello)
scala> val value1: Option[String] = myMap.get("key1")
value1: Option[String] = Some(hello)
scala> val value2: Option[String] = myMap.get("key2")
value2: Option[String] = None
scala> var re = value1.get
re: String = hello
可以看出因为map中存在键为key1的数据,所以调用map的get方法返回的是Option类型的实例 Some(hello) ;而键key2不存在,返回的是Option类型的另一个实例None。对于Some[T],通过调用它的get方法,可以获取T值。
显然在实际使用时,我们并不知道返回的Option对象的值是Some还是None,所以并不能不经过判断直接调用get,这有很多种解决方法:
1、先判断值是否是None,如:
scala> val myMap: Map[String, String] = Map("key1" -> "hello")
myMap: Map[String,String] = Map(key1 -> hello)
scala> val value1: Option[String] = myMap.get("key1")
value1: Option[String] = Some(hello)
scala> if (value1!=None){
| print(value1.get)
| }
hello
判断一个Option对象的值是否为None,最好的方式是调用Option的isEmpty方法,如:
scala> val myMap: Map[String, String] = Map("key1" -> "hello")
myMap: Map[String,String] = Map(key1 -> hello)
scala> val value1: Option[String] = myMap.get("key1")
value1: Option[String] = Some(hello)
scala> if (!value1.isEmpty) print(value1.get)
hello
不过在scala中,还有比使用if语句更好的方式,就是利用scala的match功能。
2、使用match功能,如
scala> val myMap: Map[String, String] = Map("key1" -> "hello")
myMap: Map[String,String] = Map(key1 -> hello)
scala> val value1: Option[String] = myMap.get("key1")
value1: Option[String] = Some(hello)
scala> val re = value1 match{
| case Some(v) => v
| case None => "?"
| }
re: String = hello
可以看出,使用match语句,如果不存在值返回一个默认值。
3、利用Option的getOrElse方法
Option的getOrElse方法可以设置一个默认值,当Option对象有值时,返回具体的值,当没有值时,返回默认值。如下面例子:
scala> val myMap: Map[String, String] = Map("key1" -> "hello")
myMap: Map[String,String] = Map(key1 -> hello)
scala> val re1 = myMap.get("key1").getOrElse("?")
re1: String = hello
scala> val re2 = myMap.get("key2").getOrElse("?")
re2: String = ?
Option类除了有上面介绍的get方法、isEmpty方法、getOrElse方法,还有很多其它方法,这里不再一一介绍。
4、使用for循环
上面我们提到在Scala里Option[T]实际上是一个容器,就像数组或是List一样,你可以把他看成是一个可能有零个或一个元素的List。当Option里面有东西的时候,这个List的长度是1(也就是 Some),而当你的Option里没有东西的时候,它的长度是0(也就是 None)。
如果我们把Option当成一般的List来用,并且用一个for循环来走访这个Option的时候,如果Option是None,那这个for循环里的程序代码自然不会执行,于是我们就达到了不用检查Option是否为None这件事。如下面例子:
scala> val myMap: Map[String, String] = Map("key1" -> "hello")
myMap: Map[String,String] = Map(key1 -> hello)
scala> val value1: Option[String] = myMap.get("key1")
value1: Option[String] = Some(hello)
scala> for(item<-value1) println(item);
hello
scala> val value2: Option[String] = myMap.get("key2")
value2: Option[String] = None
scala> for(item<-value2) println(item);
scala>
从上面例子可以看出,因为value2中没有值,所以第二个for循环中的代码就没有被执行。
(六)元组类型
元组是scala中的一种数据类型,它是不同类型值的聚集,它可以将不同类型的值存放在一个变量中保存。如下面例子:
scala> var data=("hello",12,true)
data: (String, Int, Boolean) = (hello,12,true)
scala> var data: (String, Int, Boolean) =("hello",12,true)
data: (String, Int, Boolean) = (hello,12,true)
上面代码定义了一个元组类型的变量data,值为("hello",12,true),可以看出元组的类型名并不是统一的一个名称,而是跟值有关系。上面元组变量data对应的类型名为(String, Int, Boolean)。
元组最大的方便之处就可以将多个值放在一起,后面可以很方便的提取其中的各个值到多个单独的变量中。如下面例子:
scala> var (a,b,c) =data
a: String = hello
b: Int = 12
c: Boolean = true
可以看出,通过 var (a,b,c) =data 这样的操作,将data元组中的3个值提取到了a,b,c三个独立的变量中。这个特性尤其在方法返回多个值时很有用。
(七)符号类型
scala的符号类型(名称为Symbol),主要其标识作用。可以采用如下的方式定义符号类型:
scala> var s1 = 'tag
s1: Symbol = 'tag
scala> var s2:Symbol = 'tag
s2: Symbol = 'tag
在标识符前面加上’就定义了一个符号类型的值。上面代码定义了两个符号类型变量,s1没有显示声明类型名,由scala自动推断出来;s2显示的声明类型为Symbol。
四、小结
本文对scala的基本语法、结构化语句、数据类型等知识进行了总结,在后面的文章中会对scala面向对象、面向函数的编程特性进行介绍。