1.声明值和变量
val变量
scala> val myStr = "Hello World!" //尽管我们在第1行代码的声明中,没有给出myStr是String类型,但是,Scala具有“类型推断”能力,可以自动推断出变量的类型。
myStr: String = Hello World!
当然,我们也可以:
scala> val myStr2 : String = "Hello World!"
myStr2: String = Hello World!
也可以:
scala> val myStr3 : java.lang.String = "Hello World!" //String类型全称是java.lang.String,也就是说,Scala的字符串是由Java的String类来实现的。
myStr3: String = Hello World!
因为myStr是val变量,因此,一旦初始化以后,就不能再次赋值,所以,下面我们执行的再次赋值操作会报错:
scala> myStr = "Hello Scala!"
<console>:27: error: reassignment to val myStr = "Hello Scala!" ^
var变量
如果一些变量,需要在初始化以后还要不断修改它的值(比如商品价格),则需要声明为var变量。
scala> var myPrice : Double = 9.9
myPrice: Double = 9.9
然后,我们可以再次对myPrice进行赋值:
scala> myPrice = 10.6
myPrice: Double = 10.6
如何在Scala解释器中输入多行代码
通常而言,只要Scala解释器推断出你的代码还没有结束,解释器就会在下一行显示一个竖线“|”,你可以继续输入剩余的代码。
scala> val myStr4 =
| "Hello World!"
myStr4: String = Hello World!
如果我们在命令提示符后面输入“val myStr5 = ”然后就回车,解释器会在下一行显示一个竖线“|”,这时如果我们发现变量名称错误,想放弃本次输入,就可以在“|”后面连续敲入两个回车,结束本次输入。
scala> val myStr5 =
|
|
You typed two blank lines. Starting a new command.scala>
2.基本数据类型和操作
基本的数据类型
Scala的数据类型包括:整数字面量、浮点数字面量、布尔型字面量、字符字面量、字符串字面量、符号字面量、函数字面量和元组字面量。
val i = 123 //123就是整数字面量
val i = 3.14 //3.14就是浮点数字面量
val i = true //true就是布尔型字面量
val i = 'A' //'A'就是字符字面量
val i = "Hello" //"Hello"就是字符串字面量
Scala允许对“字面量”直接执行方法,比如:
5.toString() //产生字符串"5"
"abc".intersect("bcd") //输出"bc"。intersect()方法用来输出两个字符串中都存在的字符。
操作符
在Scala中,可以使用加(+)、减(-) 、乘(*) 、除(/) 、余数(%)等操作符,而且,这些操作符就是方法。例如,5 + 3和(5).+(3)是等价的,也就是说:
a 方法 b
等价于
a.方法(b)
实例:
scala> val sum1 = 5 + 3 //实际上调用了 (5).+(3)
sum1: Int = 8scala> val sum2 = (5).+(3) //可以发现,写成方法调用的形式,和上面得到相同的结果sum2: Int = 8
scala> var i = 5;
i: Int = 5
scala> i += 1 //将i递增
scala> println(i)
6
3.range
(1)创建一个从1到5的数值序列,包含区间终点5,步长为1
scala> 1 to 5
res0:scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)
上面的代码,也可以用下面的代码来实现:
scala> 1.to(5)
res0:scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)
(2)创建一个从1到5的数值序列,不包含区间终点5,步长为1
scala> 1 until 5
res1: scala.collection.immutable.Range = Range(1, 2, 3, 4)
(3)创建一个从1到10的数值序列,包含区间终点10,步长为2
scala> 1 to 10 by 2
res2: scala.collection.immutable.Range = Range(1, 3, 5, 7, 9)
4.print输出
print("My name is:")
print("Ziyu")
结果
My name is Ziyu
实现换行的效果,就要采用println语句
println("My name is:")
println("Ziyu")
结果
My name is
Ziyu
此外,Scala还带有C语言风格的格式化字符串的printf函数:
val i = 5;
val j = 8;
printf("My name is %s. I hava %d apples and %d eggs.\n","Ziyu",i,j)
上面语句执行后会得到如下结果:
My name is Ziyu. I hava 5 apples and 8 eggs.
5.读写文件
把数据写入文本文件
Scala需要使用java.io.PrintWriter实现把数据写入到文本文件。
scala> import java.io.PrintWriter
import java.io.PrintWriter //这行是Scala解释器执行上面语句后返回的结果
scala> val out = new PrintWriter("output.txt")
out: java.io.PrintWriter = java.io.PrintWriter@25641d39 //这行是Scala解释器执行上面语句后返回的结果
scala> for (i <- 1 to 5) out.println(i)
scala> out.close() //注意:必须要执行out.close()语句,才会看到output.txt文件被生成
如果我们是进入“/home/hadoop”用户目录,则上面代码执行结束后,可以在目录“/home/hadoop/”下找到新生成的这个output.txt文件。
为了查看这个文件
cd ~
ls
“~”就表示当前用户的工作目录“/home/hadoop/”目录。
上面命令执行后,就可以发现,“/home/hadoop/”目录下有个新生成的这个output.txt文件,下面使用cat命令查看该文件内容:
cat output.txt
如果我们想把文件保存到一个指定的目录下,只需要改变文件路径,代码如下:
scala> import java.io.PrintWriter
import java.io.PrintWriter //这行是Scala解释器执行上面语句后返回的结果
scala> val out = new PrintWriter("/usr/local/scala/mycode/output.txt")
out: java.io.PrintWriter = java.io.PrintWriter@25641d39 //这行是Scala解释器执行上面语句后返回的结果
scala> for (i <- 1 to 5) out.println(i)
scala> out.close()
上述过程执行结束后,就可以到“/usr/local/scala/mycode/”这个目录下找到output.txt文件。
读取文本文件中的行
可以使用Scala.io.Source的getLines方法实现对文件中所有行的读取。
scala> import scala.io.Source
scala> val inputFile = Source.fromFile("output.txt", "UTF-8")
scala> val lines = inputFile.getLines //返回的结果是一个迭代器
scala> for (line <- lines) println(line)
scala>val lines = source.getLines().toArray
scala> println(lines.size)
6.条件、循环
条件语句
val x = 6
if (x>0) {println("This is a positive number")
} else {
println("This is not a positive number")
}
和Java一样,if语句可以采用各种嵌套的形式,比如:
val x = 3
if (x>0) {
println("This is a positive number")
} else if (x==0) {
println("This is a zero")
} else {
println("This is a negative number")
}
但是,有一点与Java不同的是,Scala中的if表达式的值可以赋值给变量,比如:
val x = 6
val a = if (x>0) 1 else -1
while循环
var i = 9
while (i > 0) {
i -= 1
printf("i is %d\n",i)
}
当然也会有do-while语句,如下:
var i = 0
do {
i += 1
println(i)
}while (i<5)
for循环
Scala中的for循环语句格式如下:
for (变量<-表达式) 语句块
其中,“变量<-表达式”被称为“生成器(generator)”。
下面给出一个实例:
for (i <- 1 to 5) println(i) //i不需要提前进行变量声明,可以在for语句括号中的表达式中直接使用。语句中,“<-”表示,之前的i要遍历后面1到5的所有值。
结果:
1
2
3
4
5
for (i <- 1 to 5 by 2) println(i) //设置步长为2
结果:
1
3
5
只希望输出1到5之中的所有偶数
for (i <- 1 to 5 if i%2==0) println(i)
结果:
2
4
Scala也支持“多个生成器”的情形,可以用分号把它们隔开,比如:
for (i <- 1 to 5; j <- 1 to 3) println(i*j)
结果:
1
2
3
2
4
6
3
6
9
4
8
12
5
10
15
也可以给每个生成器都添加一个“守卫”,如下:
for (i <- 1 to 5 if i%2==0; j <- 1 to 3 if j!=i) println(i*j)
结果:
2
6
4
8
12
有时候,我们需要对上述过滤后的结果进行进一步的处理,这时,就可以采用yield关键字,对过滤后的结果构建一个集合。比如,我们可以采用以下语句:
scala> for (i <- 1 to 5 if i%2==0) yield i
res3: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4)
上面这种带有yield关键字的for循环,被称为“for推导式”。这个概念源自函数式编程,也就是说,通过for循环遍历一个或多个集合,对集合中的元素进行“推导”,从而计算得到新的集合,用于后续的其他处理。
7.数据结构
数组
需要注意的是,在Scala中,对数组元素的应用,是使用圆括号,而不是方括号,也就是使用intValueArr(0),而不是intValueArr[0],这个和Java是不同的。
下面我们声明一个字符串数组,如下:
val myStrArr = new Array[String](3) //声明一个长度为3的字符串数组,每个数组元素初始化为null
myStrArr(0) = "BigData"
myStrArr(1) = "Hadoop"
myStrArr(2) = "Spark"
for (i <- 0 to 2) println(myStrArr(i))
实际上,Scala提供了更加简洁的数组声明和初始化方法,如下:
val intValueArr = Array(12,45,33)
val myStrArr = Array("BigData","Hadoop","Spark")
从上面代码可以看出,都不需要给出数组类型,Scala会自动根据提供的初始化数据来推断出数组的类型。
列表
下面我们首先声明一个列表:
val intList = List(1,2,3)
列表有头部和尾部的概念,
可以使用intList.head来获取上面定义的列表的头部,值是1,
使用intList.tail来获取上面定义的列表的尾部,值是List(2,3),
可以看出,头部是一个元素,而尾部则仍然是一个列表。
由于列表的头部是一个元素,所以,我们可以使用::操作,在列表的头部增加新的元素,得到一个新的列表,如下:
val intList = List(1,2,3)
val intListOther = 0::intList
//注意,上面操作执行后,intList不会发生变化,依然是List(1,2,3),intListOther是一个新的列表List(0,1,2,3)
::操作符是右结合的,因此,如果要构建一个列表List(1,2,3),实际上也可以采用下面的方式:
val intList = 1::2::3::Nil //Nil表示空列表。
我们也可以使用:::操作符对不同的列表进行连接得到新的列表,比如:
val intList1 = List(1,2)
val intList2 = List(3,4)
val intList3 = intList1:::intList2
//注意,执行上面操作后,intList1和intList2依然存在,intList3是一个全新的列表。
Scala还为列表提供了一些常用的方法,比如,如果要实现求和,可以直接调用sum方法,如下:
intList.sum
元组(tuple)
元组和列表不同,列表中各个元素必须是相同类型,而元组可以包含不同类型的元素。
声明一个名称为tuple的元组:
scala> val tuple = ("BigData",2015,45.0)
tuple: (String, Int, Double) = (BigData,2015,45.0) //这行是Scala解释器返回的执行结果
scala> println(tuple._1)
BigData
val t = (4,3,2,1)
val sum = t._1 + t._2 + t._3 + t._4
集(set)
集(set)是不重复元素的集合。列表中的元素是按照插入的先后顺序来组织的,但是,”集”中的元素并不会记录元素的插入顺序,而是以“哈希”方法对元素的值进行组织,所以,它允许你快速地找到某个元素。
集包括可变集和不可变集,缺省情况下创建的是不可变集,通常我们使用不可变集。
下面我们用默认方式创建一个不可变集,如下(在Scala解释器中执行):
scala> val mySet = Set("Hadoop","Spark")
mySet: scala.collection.immutable.Set[String] = Set(Hadoop, Spark)
scala> println(mySet.contains("Spark"))
true
如果要声明一个可变集,则需要引入scala.collection.mutable.Set包,具体如下(在Scala解释器中执行):
scala> import scala.collection.mutable.Set
import scala.collection.mutable.Set
scala> val myMutableSet = Set("Database","BigData")
myMutableSet: scala.collection.mutable.Set[String] = Set(BigData, Database)
scala> myMutableSet += "Cloud Computing"
res0: myMutableSet.type = Set(BigData, Cloud Computing, Database)
scala> println(myMutableSet)
Set(BigData, Cloud Computing, Database)
上面代码中,我们声明myMutableSet为val变量(不是var变量),由于是可变集,因此,可以正确执行myMutableSet += “Cloud Computing”,不会报错。
注意:虽然可变集和不可变集都有添加或删除元素的操作,但是,二者有很大的区别。对不可变集进行操作,会产生一个新的集,原来的集并不会发生变化。 而对可变集进行操作,改变的是该集本身,
映射(Map)
在Scala中,映射(Map)是一系列键值对的集合,也就是,建立了键和值之间的对应关系。在映射中,所有的值,都可以通过键来获取。
映射包括可变和不可变两种,默认情况下创建的是不可变映射,如果需要创建可变映射,需要引入scala.collection.mutable.Map包。
创建映射
我们创建一个不可变映射:
val university = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")
如果要获取映射中的值,可以通过键来获取,如下:
println(university("XMU"))
上面代码通过”XMU”这个键,可以获得值Xiamen University。
如果要检查映射中是否包含某个值,可以使用contains方法,如下:
val xmu = if (university.contains("XMU")) university("XMU") else 0
println(xmu)
上面我们定义的是不可变映射,是无法更新映射中的元素的,也无法增加新的元素。如果要更新映射的元素,就需要定义一个可变的映射,如下:
import scala.collection.mutable.Map
val university2 = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")
university2("XMU") = "Ximan University" //更新已有元素的值
university2("FZU") = "Fuzhou University" //添加新元素
也可以使用+=操作来添加新的元素,如下:
university2 + = ("TJU"->"Tianjin University") //添加一个新元素
university2 + = ("SDU"->"Shandong University","WHU"->"Wuhan University") //同时添加两个新元素
循环遍历映射
循环遍历映射,是经常需要用到的操作,基本格式是:
for ((k,v) <- 映射) 语句块
实例:
for ((k,v) <- university) printf("Code is : %s and name is: %s\n",k,v)
结果如下:
Code is : XMU and name is: Xiamen University
Code is : THU and name is: Tsinghua University
Code is : PKU and name is: Peking University
或者,也可以只遍历映射中的k或者v。比如说,我们只想把所有键打印出来:
for (k<-university.keys) println(k)
XMU //打印出的结果
THU //打印出的结果
PKU //打印出的结果
再比如说,我们只想把所有值打印出来:
for (v<-university.values) println(v)
Xiamen University //打印出的结果
Tsinghua University //打印出的结果
Peking University //打印出的结果
以下才是重点
一、类
简单的类
最简单的类的定义形式是:
class Counter{
//这里定义类的字段和方法
}
然后,就可以使用new关键字来生成对象:
new Counter //或者new Counter()
给类增加字段和方法
下面我们给这个类增加字段和方法:
class Counter {
private var value = 0
def increment(): Unit = { value += 1}
def current(): Int = {value}
}
在上面定义中,我们把value字段设置为private,这样它就成为私有字段。如果字段前面什么修饰符都没有,就默认是public,外部可以访问该字段。
- 对于方法的定义,是通过def实现的。上面的代码
“def increment(): Unit = { value += 1}”
中,increment()是方法,没有参数,冒号后面的Unit是表示返回值的类型,在Scala中不返回任何值,那么就用Unit表示。 - 方法的返回值,不需要靠return语句,方法里面的最后一个表达式的值就是方法的返回值,比如,上面current()方法里面只有一条语句“value”,那么,value的值就是该方法的返回值。
- 因为increment()方法只是对value的值进行了增加1的操作,并没有返回任何值,所以,返回值类型是Unit。
getter和setter方法
下面我们来看一下如何给类中的字段设置值以及读取值。我们知道,在Java中,这是通过getter和setter方法实现的。在Scala中,也提供了getter和setter方法的实现,但是并没有定义成getXxx和setXxx。
我们继续修改TestCounterJVM.scala文件:
class Counter {
var value = 0 //注意这里没有private修饰符,从而让这个变量对外部可见
def increment(step: Int): Unit = { value += step}
def current(): Int = {value}
}
object MyCounter{
def main(args:Array[String]){
val myCounter = new Counter
println(myCounter.value) //不是用getXxx获取字段的值
myCounter.value = 3 //不是用setXxx设置字段的值
myCounter.increment(1) //这里设置步长为1,每次增加1
println(myCounter.current)
}
}
编译执行这个文件,就可以得到两行执行结果,第一行是0,第二行是4。
但是,我们都知道,在Java中,是不提倡设置这种公有(public)字段的,一般都是把value字段设置为private,然后提供getter和setter方法来获取和设置字段的值。那么,到了Scala中该怎么做呢?
我们先把value字段声明为private,看看会出现什么效果,继续修改TestCounterJVM.scala文件:
class Counter {
private var value = 0 //增加了private修饰符,成为私有字段
def increment(step: Int): Unit = { value += step}
def current(): Int = {value}
}
object MyCounter{
def main(args:Array[String]){
val myCounter = new Counter
println(myCounter.value) //不是用getXxx获取字段的值
myCounter.value = 3 //不是用setXxx设置字段的值
myCounter.increment(1) //这里设置步长为1,每次增加1
println(myCounter.current)
}
}
现在我们去用scalac命令编译上面的代码,就会报错,会出现“error:variable value in class Counter cannot be accessed in Counter”这样的错误信息。因为,value字段前面用了修饰符private,已经成为私有字段,外部是无法访问的。
那么,value变成私有字段以后,Scala又没有提供getter和setter方法,怎么可以访问value字段呢?解决方案是,在Scala中,可以通过定义类似getter和setter的方法,分别叫做value和value_=,(value负责“读取”;“value_=”负责修改; 注意,“value_=”就职它的名字,名字里面含有等号=,千万不要误解了这个=)具体如下:
class Counter {
private var privateValue = 0 //这是私有字段,如何修改这个私有字段呢?
def value = privateValue //通过value可以获取这个私有字段privateValue的值
def value_=(newValue: Int){
if (newValue > 0) privateValue = newValue
}
//通过调用value_=这个方法,可以修改privateValue的值
def increment(step: Int): Unit = { value += step}
def current(): Int = {value}
}
object MyCounter{
def main(args:Array[String]){
val myCounter = new Counter
println(myCounter.value) //打印value的值(也就是privateValue的初始值)
myCounter.value_=(3) //调用value_=()方法,修改privateValue的值
println(myCounter.value) //打印value的新值(也就是privateValue的值)
myCounter.increment(1) //这里设置步长为1,每次增加1
println(myCounter.current)
}
}
编译执行这个文件,就可以得到三行执行结果,第一行是0,第二行是3,第三行是4。
注意
定义方法时可以def current(step:Int):Int={value}
也可以简写为def current():Int=value
还可以简写为def current:Int=value
我们写代码时不提倡简写,但是别人喜欢,所以你得看懂。
当简写为def current:Int=value
这种的时候,由于current中没有括号,所以你调用current方法时不能counter.current(),只能用counter.current这种没有括号的。
当简写为def current():Int=value
这种current()带有括号,所以你可以调用current方法时既可以counter.current(),也可以counter.current
构造器
- Scala构造器包含1个主构造器和若干个(0个或多个)辅助构造器。
- 我们首先认识一下辅助构造器。辅助构造器的名称为this,每个辅助构造器都必须调用一个此前已经定义的辅助构造器或主构造器。
class Counter(var name:String)
和class Counter(name:String)
有巨大的区别。class Counter(var name:String)
是生成了一个主构造器,而且内部会自动生成一个var name变量,而class Counter(name:String)
仅仅是传参的作用- 类似于
class Counter(var name:String)
这种,在声明类的时候传参时使用了var的就是主构造器
scala> class Counter(var name:String)
scala> var mycounter = new Counter('Runner') //因为你传入了'Runner',所以相当于var name = Runner
scala>println(mycounter.name)
Runner
scala>mycounter.name_=('Timer') //name_=是调用setter的方法,修改name的值
scala>mycounter.name='Timer' //等价于上一条语句,更提倡用上一条语句,但别人这么写你得看得懂
mycounter.name:String=Timer
1.主构造器和辅助构造器实例:
class Counter {
private var value = 0 //value用来存储计数器的起始值
private var name = "" //表示计数器的名称
private var mode = 1 //mode用来表示计数器类型(比如,1表示步数计数器,2表示时间计数器)
println('the main constructor')
//辅助构造器使用时必须调用此前已经定义好的主构造器或者辅助构造器,下面这个辅助构造器是第一个辅助构造器,之前没有定义好的辅助构造器,所以使用时调用之前已经定义好的主构造器
def this(name: String){ //第一个辅助构造器
this() //调用之前已经定义好的主构造器
this.name = name
printf("the first auxiliary constructor,name:%s\n",name)
}
def this (name: String, mode: Int){ //第二个辅助构造器
this(name) //调用之前已经定义好的(前一个)辅助构造器
this.mode = mode
printf("the second auxiliary constructor,name:%s,step:%d\n",name,step)
}
def increment(step: Int): Unit = { value += step}
def current(): Int = {value}
}
scala>:load /user/local/scala/mycode/Counter2.scala
...
scala>val c1=new Counter
the main constructor //这是输出
scala>val c2=new Counter('the 2nd Counter') //调用第一个辅助构造器
the main constructor
the first auxiliary constructor,name:the 2nd Counter
scala>val c3=new Counter('the 3rd Counter',2)
the main constructor
the first auxiliary constructor,name:the 3rd Counter
the second auxiliary constructor,name:the 3rd Counter,step:2
2.辅助构造器实例:
class Counter {
private var value = 0 //value用来存储计数器的起始值
private var name = "" //表示计数器的名称
private var mode = 1 //mode用来表示计数器类型(比如,1表示步数计数器,2表示时间计数器)
def this(name: String){ //第一个辅助构造器
this() //调用主构造器
this.name = name
}
def this (name: String, mode: Int){ //第二个辅助构造器
this(name) //调用前一个辅助构造器
this.mode = mode
}
def increment(step: Int): Unit = { value += step}
def current(): Int = {value}
def info(): Unit = {printf("Name:%s and mode is %d\n",name,mode)}
}
object MyCounter{
def main(args:Array[String]){
val myCounter1 = new Counter //主构造器
val myCounter2 = new Counter("Runner") //第一个辅助构造器,计数器的名称设置为Runner,用来计算跑步步数
val myCounter3 = new Counter("Timer",2) //第二个辅助构造器,计数器的名称设置为Timer,用来计算秒数
myCounter1.info //显示计数器信息
myCounter1.increment(1) //设置步长
printf("Current Value is: %d\n",myCounter1.current) //显示计数器当前值
myCounter2.info //显示计数器信息
myCounter2.increment(2) //设置步长
printf("Current Value is: %d\n",myCounter2.current) //显示计数器当前值
myCounter3.info //显示计数器信息
myCounter3.increment(3) //设置步长
printf("Current Value is: %d\n",myCounter3.current) //显示计数器当前值
}
}
编译执行上述代码后,得到如下结果:
Name: and mode is 1
Current Value is: 1
Name:Runner and mode is 1
Current Value is: 2
Name:Timer and mode is 2
Current Value is: 3
3.主构造器实例:
对于上面给计数器设置name和mode的例子,刚才我们是使用辅助构造器来对name和mode的值进行设置,现在我们重新来一次,这次我们转而采用主构造器来设置name和mode的值。
class Counter(val name: String, val mode: Int) {
private var value = 0 //value用来存储计数器的起始值
def increment(step: Int): Unit = { value += step}
def current(): Int = {value}
def info(): Unit = {printf("Name:%s and mode is %d\n",name,mode)}
}
object MyCounter{
def main(args:Array[String]){
val myCounter = new Counter("Timer",2)
myCounter.info //显示计数器信息
myCounter.increment(1) //设置步长
printf("Current Value is: %d\n",myCounter.current) //显示计数器当前值
}
}
编译执行上述代码后,得到如下结果:
Name:Timer and mode is 2
Current Value is: 1
二、对象
单例对象
下面是单例对象的定义:
object Person {
private var lastId = 0 //一个人的身份编号
def newPersonId() = {
lastId +=1
lastId
}
}
- 从上面的定义可以看出,单例对象的定义和类的定义很相似,明显的区分是,用object关键字,而不是用class关键字。
- 假设有一个班级人员管理系统,每当新来一个班级成员,就给分配一个身份编号。当第一个人加入班级时,你就可以调用Person.newPersonId()获得身份编号。
现在,我们测试运行这个实例:
在test.scala文件中输入以下内容:
object Person {
private var lastId = 0 //一个人的身份编号
def newPersonId() = {
lastId +=1
lastId //作为返回值返回
}
}
printf("The first person id is %d.\n",Person.newPersonId())
printf("The second person id is %d.\n",Person.newPersonId())
printf("The third person id is %d.\n",Person.newPersonId())
保存文件并退出vim编辑器,然后,在Shell命令提示符下输入scala命令运行上面代码:
执行后,屏幕上会显示以下结果:
The first person id is 1.
The second person id is 2.
The third person id is 3.
注意,对于一个Scala应用程序而言,必须包含main方法,由于上面代码中没有包含main方法,因此,不能使用scalac命令进行编译,而是直接使用scala命令运行代码,就可以得到结果。
伴生对象
- 在Java中,我们经常需要用到同时包含实例方法和静态方法的类,在Scala中可以通过伴生对象来实现。
- 当单例对象与某个类具有相同的名称时,它被称为这个类的“伴生对象”。(例如在同一个文件中,你定义了一个Person的单例对象object Person{},而后又定义了一个Person的类 class Person(val name:String){ }那么它俩互为伴生关系)。
- 类和它的伴生对象必须存在于同一个文件中,而且可以不需要进行实例化的条件下相互访问对方内部的成员变量和成员方法(私有的也可以)。
在“/usr/local/scala/mycode”目录下,用vim编辑器重新创建一个test.scala,在该文件中输入如下代码:
class Person(val name:String) {
private val id = Person.newPersonId() //访问伴生对象的方法,半生方法可以不需要进行实例化的条件下相互访问对方内部的成员变量和成员方法
def info() { printf("The id of %s is %d.\n",name,id)}
}
object Person {
private var lastId = 0 //一个人的身份编号
private def newPersonId() = {
lastId +=1
lastId
}
//在person单例对象里面定义了一个main函数(scalac需要一个main函数为入口)
def main(args: Array[String]){
val person1 = new Person("Ziyu") //这个Person是class Person
val person2 = new Person("Minxing")
person1.info()
person2.info()
}
}
然后,在Shell命令提示符状态下,输入以下命令编译并执行:
运行结果如下:
The id of Ziyu is 1.
The id of Minxing is 2.
- 从上面结果可以看出,伴生对象中定义的newPersonId()实际上就实现了Java中静态(static)方法的功能,所以,实例化对象person1调用newPersonId()返回的值是1,实例化对象person2调用newPersonId()返回的值是2。
- 我们说过,Scala源代码编译后都会变成JVM字节码,实际上,在编译上面的源代码文件以后,在Scala里面的class和object在Java层面都会被合二为一,class里面的成员成了实例成员,object成员成了static成员。
apply方法
1.自动调用类里面的apply方法
如果你定义了一个类,还在这个类里面定义了一个apply方法,那么当你给类生成一个带有参数的实例时,它会自动去调用apply方法(看不懂,看下面)
例如:
我们看到,你定义了一个TestApplyClass类,还在这个类里面定义了一个apply方法,那么当你给TestApplyClass类生成一个带有参数的实例时( 也就是myObject(“Hello Apply”) ),它会自动去调用TestApplyClass类里面的apply方法(也就是把参数自动传给了TestApplyClass类里面的apply方法)
2.自动调用某个类的伴生对象的apply方法
apply方法和update方法
下面我们举几个apply方法的完整实例:
实例1:
在Linux系统的“/usr/local/scala/mycode/test.scala”文件中输入以下代码:
class TestApplyClass {
def apply(param: String): String = {
println("apply method called, parameter is: " + param)
}
}
val myObject = new TestApplyClass
println(myObject("param1"))
然后,在Linux系统的Shell命令提示符下运行scala命令:
运行后会得到以下结果:
apply method is called, parameter is:param1
可以看出,当你用myObject(“param1”)时apply方法确实被调用了
实例2:
上面是类中定义了apply方法,下面看一个在单例对象中定义apply方法的例子:
object TestApplySingleObject {
def apply(param1: String, param2: String): String = {
println("apply method called")
param1 + " and " + param2
}
}
val group = TestApplySingleObject("Zhangfei", "Liubei")
println(group)
把上面代码放入到test.scala文件中测试执行后,可以得到如下结果:
apply method called
Zhangfei and Liubei
可以看出,在执行TestApplySingleObject(“Zhangfei”, “Liubei”)时调用了apply方法,并且把“Zhangfei and Liubei”作为返回值,赋值给group变量,因此,println(group)语句会打印出“Zhangfei and Liubei”。
实例3:
下面我们测试一个伴生类和伴生对象中的apply方法实例。请在Linux系统的“/usr/local/scala/mycode/test.scala”文件中输入以下代码:
class TestApplyClassAndObject {
}
class ApplyTest{
def apply() = println("apply method in class is called!")
def greetingOfClass: Unit ={
println("Greeting method in class is called.")
}
}
object ApplyTest{
def apply() = {
println("apply method in object is called")
new ApplyTest()
}
}
object TestApplyClassAndObject{
def main (args: Array[String]) {
val a = ApplyTest() //这里会调用伴生对象中的apply方法
a.greetingOfClass
a() // 这里会调用伴生类中的apply方法
}
}
首先使用scalac编译命令对test.scala进行编译,然后,使用scala命令运行,具体如下:
上述代码执行后得到以下结果:
apply method in object is called
Greeting method in class is called.
apply method in class is called!
从上面代码可以看出,当我们执行val a = ApplyTest()时,会导致apply方法的调用并返回该方法调用的值,也就是ApplyTest的实例化对象。当执行a()时,又会导致调用伴生类的apply方法。
实例4
事实上,在数组内部是自定义了apply方法的,我们定义数组就是运用了apply的方法
val myStrArr = Array("BigData","Hadoop","Spark")
也就是说,不需要new关键字,不用构造器,直接给对象传递3个参数,Scala就会转换成对apply方法的调用,也就是调用Array类的伴生对象Array的apply方法,完成数组的初始化。
实例五:
讲到这里,我们已经理解了apply方法,这样,我们现在解释Scala中伴生对象的一个重要用途,你就不会存在理解障碍了。在Scala中,伴生对象有一个重要用途,那就是,我们通常将伴生对象作为工厂使用,这样就不需要使用关键字new来创建一个实例化对象了,具体实例如下:
class Car(name: String){
def info() {println("Car name is "+ name)}
}
object Car {
def apply(name: String) = new Car(name) //apply方法会调用伴生类Car的构造方法创建一个Car类的实例化对象
}
object MyTest{
def main (args: Array[String]) {
val mycar = Car("BMW") //这里会调用伴生对象中的apply方法,apply方法会创建一个Car类的实例化对象
mycar.info()
}
}
首先使用scalac编译命令对test.scala进行编译,然后,使用scala命令运行,具体如下:
上述代码执行后得到以下结果:
Car name is BMW
三、继承
Scala中的继承与Java有着显著的不同:
(1)重写一个非抽象方法必须使用override修饰符。
(2)只有主构造器可以调用超类的主构造器。
(3)在子类中重写超类的抽象方法时,不需要使用override关键字。
(4)可以重写超类中的字段。
Scala和Java一样,不允许类从多个超类继承,因此,下面我们只讨论继承自一个类的情形。
抽象类
以汽车为例子,首先我们创建一个抽象类,让这个抽象类被其他类继承。
abstract class Car{ //是抽象类,不能直接被实例化
val carBrand: String //字段没有初始化值,就是一个抽象字段
def info() //没有方法体,就是抽象方法,不需要使用abstract关键字
def greeting() {println("Welcome to my car!")} //有方法体,这就是具体方法
}
关于上面的定义,说明几点:
(1)定义一个抽象类,需要使用关键字abstract。
(2)定义一个抽象类的抽象方法,也不需要关键字abstract,只要把方法体空着,不写方法体就可以。
(3)抽象类中定义的字段,只要没有给出初始化值,就表示是一个抽象字段,但是,抽象字段必须要声明类型,比如:val carBrand: String,就把carBrand声明为字符串类型,这个时候,不能省略类型,否则编译会报错。
扩展类
- 子类继承了父类以后,如果实现了父类里面的抽象成员,那么override这个字段是可选的;子类继承了父类以后,如果重载父类里面的非抽象成员(也就是重载父类里面已经实现了的那些非抽象成员),那么override这个字段是必须有的。
- 子类只能重载父类的val类型字段,不能重载var类型字段
抽象类不能直接被实例化,所以,下面我们定义几个扩展类,它们都是扩展了Car类,或者说继承自Car类。
实例:
请登录Linux系统,进入shell命令提示符状态,然后,输入以下命令进入“/usr/local/scala/mycode”目录,打开vim编辑器:
在test.scala文件中输入以下内容:
abstract class Car{
val carBrand: String //抽象字段
def info() //抽象方法
def greeting() {println("Welcome to my car!")} //已经实现了的具体方法
}
class BMWCar extends Car {
override val carBrand = "BMW" //实现了父类的抽象字段
def info() {printf("This is a %s car. It is expensive.\n", carBrand)} //实现了父类的抽象方法
override def greeting() {println("Welcome to my BMW car!")} //重新实现了父类的具体方法,必须要有override
}
class BYDCar extends Car {
override val carBrand = "BYD"
def info() {printf("This is a %s car. It is cheap.\n", carBrand)}
override def greeting() {println("Welcome to my BYD car!")}
}
object MyCar {
def main(args: Array[String]){
val myCar1 = new BMWCar()
val myCar2 = new BYDCar()
myCar1.greeting()
myCar1.info()
myCar2.greeting()
myCar2.info()
}
}
保存文件并退出vim编辑器,然后,在Shell命令提示符下输入scala命令运行上面代码:
执行后,屏幕上会显示以下结果:
Welcome to my BMW car!
This is a BMW caar. It is expensive.
Welcome to my BYD car!
This is a BYD caar. It is cheap.
四、特质
- Java中提供了接口,允许一个类实现任意数量的接口。在Scala中没有接口的概念,而是提供了“特质(trait)”。
- 特质和类事实上就非常类似,我们之所以要弄个特质出来,就是为了实现多重继承(Scala中,一个类只能继承自一个超类,却可以实现多个特质,从而重用特质中的方法和字段,实现了多重继承。)
- Scala的特质,可以同时拥有抽象方法和具体方法(不要误以为它只能有抽象方法)。
特质的定义
特质和类非常相似,有区别的是,特质定义使用关键字trait。
trait CarId{
var id: Int //抽象字段
def currentId(): Int //定义了一个抽象方法(没有方法体的方法,默认就是抽象方法)
}
//注意,特质中没有方法体的方法,默认就是抽象方法,抽象方法不需要使用abstract关键字。
这就是一个特质,和类相当类似,就是把class改成trait。这个特质里面包含一个抽象字段id和抽象方法currentId。
把特质混入类中(让类继承了这些特质)
特质定义好以后,就可以使用extends或with关键字把特质混入类中。
class BYDCarId extends CarId{ //使用extends关键字
override var id = 10000 //BYD汽车编号从10000开始
def currentId(): Int = {id += 1; id} //返回汽车编号
}
class BMWCarId extends CarId{ //使用extends关键字
override var id = 20000 //BMW汽车编号从20000开始
def currentId(): Int = {id += 1; id} //返回汽车编号
}
从上面可以看到,和类的使用非常非常类似。
下面,我们完整的编译运行一下这段代码。
请登录Linux系统,进入shell命令提示符状态,然后,输入以下命令进入“/usr/local/scala/mycode”目录,打开vim编辑器:
在test.scala文件中输入以下内容:
trait CarId{
var id: Int
def currentId(): Int //定义了一个抽象方法
}
class BYDCarId extends CarId{ //使用extends关键字
override var id = 10000 //BYD汽车编号从10000开始
def currentId(): Int = {id += 1; id} //返回汽车编号
}
class BMWCarId extends CarId{ //使用extends关键字
override var id = 20000 //BMW汽车编号从10000开始
def currentId(): Int = {id += 1; id} //返回汽车编号
}
object MyCar {
def main(args: Array[String]){
val myCarId1 = new BYDCarId()
val myCarId2 = new BMWCarId()
printf("My first CarId is %d.\n",myCarId1.currentId)
printf("My second CarId is %d.\n",myCarId2.currentId)
}
}
保存后退出vim编辑器。然后,使用scalac命令编译这个代码文件,并用scala命令执行,如下:
上面命令执行后,会在屏幕输出以下结果:
My first CarId is 10001.
My second CarId is 20001.
特质可以包含具体实现
上面的实例中,特质只包含了抽象字段和抽象方法,相当于实现了类似Java接口的功能。实际上,特质也可以包含具体实现,也就是说,特质中的字段和方法不一定要是抽象的。
trait CarGreeting{
def greeting(msg: String) {println(msg)}
}
CarGreeting会把欢迎信息打印出来。
把多个特质混入类中
上面我们已经定义了两个特质,即CarId和CarGreeting。现在,我们可以把两个特质都混入到类中。
请登录Linux系统,进入shell命令提示符状态,然后,输入以下命令进入“/usr/local/scala/mycode”目录,打开vim编辑器:
在test.scala文件中输入以下内容:
trait CarId{
var id: Int
def currentId(): Int //定义了一个抽象方法
}
trait CarGreeting{
def greeting(msg: String) {println(msg)}
}
class BYDCarId extends CarId with CarGreeting{ //使用extends关键字混入第1个特质,后面可以反复使用with关键字混入更多特质
override var id = 10000 //BYD汽车编号从10000开始
def currentId(): Int = {id += 1; id} //返回汽车编号
}
class BMWCarId extends CarId with CarGreeting{ //使用extends关键字混入第1个特质,后面可以反复使用with关键字混入更多特质
override var id = 20000 //BMW汽车编号从10000开始
def currentId(): Int = {id += 1; id} //返回汽车编号
}
object MyCar {
def main(args: Array[String]){
val myCarId1 = new BYDCarId()
val myCarId2 = new BMWCarId()
myCarId1.greeting("Welcome my first car.")
printf("My first CarId is %d.\n",myCarId1.currentId)
myCarId2.greeting("Welcome my second car.")
printf("My second CarId is %d.\n",myCarId2.currentId)
}
}
保存后退出vim编辑器。然后,使用scalac命令编译这个代码文件,并用scala命令执行,如下:
上面命令执行后,会在屏幕输出以下结果:
Welcome my first car.
My first CarId is 10001.
Welcome my second car.
My second CarId is 20001.
回顾一下这句话:特质和类事实上就非常类似,我们之所以要弄个特质出来,就是为了实现多重继承(Scala中,一个类只能继承自一个超类,却可以实现多个特质,从而重用特质中的方法和字段,实现了多重继承。)
特质例题
五、模式匹配
scala中也有switch-case语句,比java要强大得多。
简单匹配
例题一:
Scala的模式匹配最常用于match语句中。下面是一个简单的整型值的匹配实例。
val colorNum = 1
val colorStr = colorNum match {
case 1 => "red"
case 2 => "green"
case 3 => "yellow"
case _ => "Not Allowed"
}
println(colorStr)
为了测试上面代码,可以直接把上面代码放入到“/usr/local/scala/mycode/test.scala”文件中,然后,在Linux系统的Shell命令提示符状态下执行下面命令:
例题二:
例题3:
类型匹配
Scala可以对表达式的类型进行匹配。
for (elem <- List(9,12.3,"Spark","Hadoop",'Hello)){
val str = elem match{
case i: Int => i + " is an int value."
case d: Double => d + " is a double value."
case "Spark"=> "Spark is found."
case s: String => s + " is a string value."
case _ => "This is an unexpected value."
}
println(str)
}
在test.scala文件中测试执行上述代码后会在屏幕上输出:
9 is an int value.
12.3 is a double value.
Spark is found.
Hadoop is a string value.
This is an unexpected value.
还可以在模式匹配中添加一些必要的处理逻辑
for (elem <- List(1,2,3,4)){
elem match {
case _ if (elem %2 == 0) => println(elem + " is even.")
case _ => println(elem + " is odd.")
}
}
上面代码中if后面条件表达式的圆括号可以不要。执行上述代码后可以得到以下输出结果:
1 is odd.
2 is even.
3 is odd.
4 is even.
六、case类的匹配
回顾:for表达式中的模式
我们之前在介绍“映射”的时候,实际上就已经接触过了for表达式中的模式。
还是以我们之前举过的映射为例子,我们创建的映射如下:
val university = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")
循环遍历映射的基本格式是:
for ((k,v) <- 映射) 语句块
对于遍历过程得到的每个值,都会被绑定到k和v两个变量上,也就是说,映射中的“键”被绑定到变量k上,映射中的“值”被绑定到变量v上。
下面给出此前已经介绍过的实例:
for ((k,v) <- university) printf("Code is : %s and name is: %s\n",k,v)
上面代码执行结果如下:
Code is : XMU and name is: Xiamen University
Code is : THU and name is: Tsinghua University
Code is : PKU and name is: Peking University
case类的匹配
case类是一种特殊的类,它们经过优化以被用于模式匹配。
case class Car(brand: String, price: Int)
val myBYDCar = new Car("BYD", 89000)
val myBMWCar = new Car("BMW", 1200000)
val myBenzCar = new Car("Benz", 1500000)
for (car <- List(myBYDCar, myBMWCar, myBenzCar)) {
car match{
case Car("BYD", 89000) => println("Hello, BYD!")
case Car("BMW", 1200000) => println("Hello, BMW!")
case Car(brand, price) => println("Brand:"+ brand +", Price:"+price+", do you want it?")
}
}
把上述代码放入test.scala文件中,运行“scala test.scala”命令执行后可以得到如下结果:
Hello, BYD!
Hello, BMW!
Brand: Benz, Price:1500000, do you want it?
六、函数式编程
6.1函数定义和高阶函数
函数字面量
字面量包括整数字面量、浮点数字面量、布尔型字面量、字符字面量、字符串字面量、符号字面量、函数字面量和元组字面量。
val i = 123 //123就是整数字面量
val i = 3.14 //3.14就是浮点数字面量
val i = true //true就是布尔型字面量
val i = 'A' //'A'就是字符字面量
val i = "Hello" //"Hello"就是字符串字面量
- 在非函数式编程语言里,函数的定义包含了“函数类型”和“值”两种层面的内容。
- 在函数式编程中,我们就可以像定义变量那样去定义一个函数,由此导致的结果是,函数也会和其他变量一样。
函数的语法
① 方法的创建规则:def 名称(参数 : 参数类型) = 函数体,如:def increment(): Unit = { value +=1}
②普通函数的创建规则:def 名称(参数 : 参数类型) :函数返回值类型=函数体,如:def counter(value: Int): Int = { value += 1}
③匿名函数的创建规则:(参数 : 参数类型) => 函数体
④省略式函数:用定义变量的方式去定义函数,用的是val而不是def
以下是你最经常见的函数:
- 最常见的无返回值的函数:
def fun1(name:String):Unit={println(name)}
和def fun1(name:String)=println(name)
和def fun1(name:String){println(name)}
,最提倡def fun1(name:String)=println(name)
- 最常见的有返回值的函数:
def counter(value: Int): Int = { value += 1}
- 匿名函数:
(num: Int) => num * 2
- 省略式函数(用定义变量的方式去定义函数):
- (无返回值的)
val fun2=(content:string) => println(content)
- (有返回值的)
val myNumFunc: Int=>Int = (num: Int) => num * 2
和val counter: Int => Int = { (value) => value += 1 }
- (有返回值的简写)
val myNumFunc = (num: Int) => num * 2
普通函数与普通函数的简写
1.现在定义一个传统类型的函数,定义的语法和我们之前介绍过的定义“类中的方法”类似:
def counter(value: Int): Int = { value += 1}
- 上面定义的函数中,传入的参数是int型,输出的结果也是int,也就是
(Int) => Int
(当参数只有一个时,那个圆括号才可以省略) - 上面的函数中,传入的值是(value),输出得结果是{ value += 1},也就是
(value) => {value += 1} //只有一条语句时,大括号可以省略
2.现在,我们就可以像定义变量那样去定义一个函数:
定义变量如下:
val num: Int = 5 //当然,Int类型声明也可以省略,因为Scala具有自动推断类型的功能
照葫芦画瓢,我们也可以按照上面类似的形式来定义函数:
val counter: Int => Int = { (value) => value += 1 }
这样,我们只要在某个需要声明函数的地方声明一个函数类型,在调用的时候传一个对应的函数字面量即可,和使用普通变量一模一样。
匿名函数、Lambda表达式
我们不需要给每个函数命名,这时就可以使用匿名函数,如下:
(num: Int) => num * 2
上面这种匿名函数的定义形式,我们经常称为“Lambda表达式”。“Lambda表达式”的形式如下:
(参数) => 表达式 //如果参数只有一个,参数的圆括号可以省略
我们可以直接把匿名函数赋值到变量中,下面是在Scala解释器中的执行过程:
没有返回值的:
scala> val fun2=(content:string) => println(content)
scala> fun2("Hadoop")
有返回值的:
scala> val myNumFunc: Int=>Int = (num: Int) => num * 2 //这行是我们输入的命令,把匿名函数定义为一个值,赋值给myNumFunc变量
scala> println(myNumFunc(3)) //myNumFunc函数调用的时候,需要给出参数的值,这里传入3,得到乘法结果是6
6
简写为:
scala> val myNumFunc = (num: Int) => num * 2
scala> println(myNumFunc(3))
6
甚至省略为:
scala> val myNumFunc = (_:Int)*2 //这条语句是我自己加的,对错不明
关于下划线"_"的使用
在spark中下划线的使用极其常见,所以你必须知道下划线的含义。
- 当某一个函数字面量仅出现一次时,我们可以用下划线来表示。
- 某一条语句如果有两个下划线,那么一定表示两个仅出现一次的函数字面量,而不是一个出现了两次的字面量(也就是两个下划线表示两个值,而不是同一个值)。
举例:
下面两个函数字面量是等价的。
x => x>0
_ > 0
高阶函数
函数本身括号里的参数仍然是一个函数,这就是高阶函数。spark里很多都是高阶函数。
我们先了解一下
实例:
1.普通方法:
//普通方法求给定两个数区间中的所有整数求和
def sumInts(a: Int, b: Int): Int = {
if(a > b) 0 else a + sumInts(a + 1, b)
}
//普通方法求连续整数的平方和
def square(x: Int): Int = x * x
def sumSquares(a: Int, b: Int): Int = {
if(a > b) 0 else square(a) + sumSquares(a + 1, b)
}
//普通方法求连续整数的关于2的幂次和
def powerOfTwo(x: Int): Int = {
if(x == 0) 1 else 2 * powerOfTwo(x-1)
} //例如求powerOfTwo(4)就是2^3
def sumPowersOfTwo(a: Int, b: Int): Int = {
if(a > b) 0 else powerOfTwo(a) + sumPowersOfTwo(a+1, b)
}
//例如求sumPowersOfTwo(2,4)就是2^1 + sumPowersOfTwo(3,4)=2^1 + 2^2 + sumPowersOfTwo(4,4)=2^1 + 2^2 + 2^3 + 0
2.高阶函数法:
高阶函数法求给定两个数区间中的所有整数求和
def sum(f: Int => Int, a: Int, b: Int): Int ={ if(a > b) 0 else f(a) + sum(f, a+1, b) } //定义了一个高阶函数sum,以函数f为参数
def self(x: Int): Int = x //定义了一个新的函数self,该函数的输入是一个整数x,然后直接输出x自身
def sumInts(a: Int, b: Int): Int = sum(self, a, b)
//高阶函数法求整数和、平方和、幂次和
def sum(f: Int => Int, a: Int, b: Int): Int = {
if(a > b) 0 else f(a) + sum(f, a+1, b)
}
def self(x: Int): Int = x
def square(x: Int): Int = x * x
def powerOfTwo(x: Int): Int = if(x == 0) 1 else 2 * powerOfTwo(x-1)
def sumInts(a: Int, b: Int): Int = sum(self, a, b)
def sumSquared(a: Int, b: Int): Int = sum(square, a, b)
def sumPowersOfTwo(a: Int, b: Int): Int = sum(powerOfTwo, a, b)
3.我们使用高阶函数法进行完整测试:
到Linux系统的“/usr/local/scala/mycode”目录下新建test.scala代码文件进行测试,文件里面包含以下内容:
def sum(f: Int => Int, a: Int, b: Int): Int = {
if(a > b) 0 else f(a) + sum(f, a+1, b)
}
def self(x: Int): Int = x
def square(x: Int): Int = x * x
def powerOfTwo(x: Int): Int = if(x == 0) 1 else 2 * powerOfTwo(x-1)
def sumInts(a: Int, b: Int): Int = sum(self, a, b)
def sumSquared(a: Int, b: Int): Int = sum(square, a, b)
def sumPowersOfTwo(a: Int, b: Int): Int = sum(powerOfTwo, a, b)
println(sumInts(1,5))
println(sumSquared(1,5))
println(sumPowersOfTwo(1,5))
然后,在Linux系统的Shell命令提示符下,运行scala命令运行test.scala代码,如下:
事实上,
你能用普通方法写出上述程序就已经很强了,高阶方法只做了解,你必须知道什么是高阶方法