Scala语言入门

前言

学习Scala之前我们先看一个简单的例子

Java

1
2
3
4
List<Product> products = new ArrayList<Product>();
for (Order order : orders) {
products.addAll(order.getProducts());
}

Scala 的:

1
def products = orders.flatMap(o => o.products)

甚至可以更简洁:

1
def products = orders.flatMap(_.products)

这样我们就可以看出Scala可以写更少的代码来实现同样的功能。

下载

下载

https://www.scala-lang.org/download/2.12.15.html

或者

下载地址

链接:https://pan.baidu.com/s/1YczOo5novINV_MimJ9Xpqg
提取码:psvm

image-20220421183614199

注意安装路径不能有空格否则报错

此时不应有 \scala\bin..\lib\jline-3.21.0.jar。

测试是否可用

1
scala -version

IDEA安装插件

image-20220419155400091

安装后重启IDEA

创建项目

image-20220419160436001

项目上右键

image-20220419161330150

添加Scala SDK

image-20220419161121807

添加我们的测试类

image-20220419161554832

代码如下

1
2
3
4
5
6
7
8
package cn.psvmc

object Test {
def main(args: Array[String]): Unit = {
val str: String = "Hello Scala"
print(str)
}
}

语法

Array

数组是编程中经常用到的数据结构,一般包括定长数组和变长数组。本教程旨在快速掌握最基础和常用的知识,因此,只介绍定长数组。

定长数组,就是长度不变的数组,在Scala中使用Array进行声明,如下:

1
2
3
4
val intValueArr = new Array[Int](3)  //声明一个长度为3的整型数组,每个数组元素初始化为0
intValueArr(0) = 12 //给第1个数组元素赋值为12
intValueArr(1) = 45 //给第2个数组元素赋值为45
intValueArr(2) = 33 //给第3个数组元素赋值为33

需要注意的是,在Scala中,对数组元素的应用,是使用圆括号,而不是方括号,也就是使用intValueArr(0),而不是intValueArr[0],这个和Java是不同的。
下面我们再声明一个字符串数组,如下:

1
2
3
4
5
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提供了更加简洁的数组声明和初始化方法,如下:

1
2
val intValueArr = Array(12,45,33)
val myStrArr = Array("BigData","Hadoop","Spark")

从上面代码可以看出,都不需要给出数组类型,Scala会自动根据提供的初始化数据来推断出数组的类型。

List

1
val intList = List(1,2,3)

列表有头部和尾部的概念,可以使用intList.head来获取上面定义的列表的头部,值是1,使用intList.tail来获取上面定义的列表的尾部,值是List(2,3),可以看出,头部是一个元素,而尾部则仍然是一个列表。

由于列表的头部是一个元素,所以,我们可以使用::操作,在列表的头部增加新的元素,得到一个新的列表,如下:

1
2
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),实际上也可以采用下面的方式:

1
val intList = 1::2::3::Nil

上面代码中,Nil表示空列表。
我们也可以使用:::操作符对不同的列表进行连接得到新的列表,比如:

1
2
3
val intList1 = List(1,2)
val intList2 = List(3,4)
val intList3 = intList1:::intList2

注意,执行上面操作后,intList1和intList2依然存在,intList3是一个全新的列表。

实际上,Scala还为列表提供了一些常用的方法,比如,如果要实现求和,可以直接调用sum方法,如下:

1
2
3
4
5
6
7
object Scala01 {
def main(args: Array[String]): Unit = {
val intList1 = List(1, 2)
val sum = intList1.sum-
println(sum)
}
}

map

1
2
3
val books = List("Hadoop", "Hive", "HDFS")
val books2 = books.map(s => s.toUpperCase)
println(books2)

结果

1
List(HADOOP, HIVE, HDFS)

flatMap

1
2
3
val books = List("Hadoop", "Hive", "HDFS")
val books2 = books.flatMap(s => s.toUpperCase)
println(books2)

结果

1
List(H, A, D, O, O, P, H, I, V, E, H, D, F, S)

flatMap把这些集合中的元素“拍扁”得到一个集合List

filter

1
2
3
val books = List("Hadoop", "Hive", "HDFS")
val books2 = books.filter(s => s.contains("Ha"))
println(books2)

结果

1
List(Hadoop)

reduce

1
2
3
4
val list = List(1, 2, 3, 4, 5)
println(list.reduceLeft(_ + _))
println(list.reduceRight(_ + _))
println(list.reduce(_ + _))

reduce和reduceLeft计算的方式是一致的

reduceLeft

1
2
3
4
1+2 = 3
3+3 = 6
6+4 = 10
10+5 = 15

reduceRight

1
2
3
4
4+5 = 9
3+9 = 12
2+12 = 14
1+14 = 15

flod

1
2
val list = List(1, 2, 3, 4, 5)
println(list.foldLeft(10)(_ + _))

结果

1
25

fold函数实现了对list中所有元素的累计操作。

fold函数需要两个参数,一个参数是初始种子值,这里是10,另一个参数是用于计算结果的累计函数,这里是累加。

执行list.fold(10)(_ + _)时,首先把初始值拿去和list中的第一个值1做加法操作,得到值11,然后再拿这个值11去和list中的第2个值2做加法操作,得到13,依此类推,一直得到最终的结果25。

Tuple

元组是不同类型的值的聚集。元组和列表不同,列表中各个元素必须是相同类型,而元组可以包含不同类型的元素。

下面我们声明一个名称为tuple的元组(为了看到声明后的效果,我们这次在Scala解释器中输入代码并执行):

1
2
3
4
val tuple = ("BigData",2015,45.0)
println(tuple._1)
println(tuple._2)
println(tuple._3)

从上述代码在Scala解释器中的执行效果可以看出,我们声明一个元组是很简单的,只需要用圆括号把多个元组的元素包围起来就可以了。
当需要访问元组中的某个元素的值时,可以通过类似tuple._1tuple._2tuple._3这种方式就可以实现。

Set

集(set)是不重复元素的集合。

列表中的元素是按照插入的先后顺序来组织的,但是,”集”中的元素并不会记录元素的插入顺序,而是以“哈希”方法对元素的值进行组织,所以,它允许你快速地找到某个元素。

集包括可变集和不可变集,缺省情况下创建的是不可变集,通常我们使用不可变集。
下面我们用默认方式创建一个不可变集,如下:

1
2
3
4
5
6
7
var mySet = Set("Hadoop", "Spark")
// 添加元素
mySet += "Scala"
// 删除元素
mySet -= "Hadoop"
println(mySet)
println(mySet.contains("Scala"))

上面声明时,如果使用val,mySet += "Scala"执行时会报错,所以需要声明为var。

如果要声明一个可变集,则需要引入scala.collection.mutable.Set包,具体如下:

1
2
3
4
5
import scala.collection.mutable.Set

val myMutableSet = Set("Database","BigData")
myMutableSet += "Cloud Computing"
println(myMutableSet)

上面代码中,我们声明myMutableSet为val变量(不是var变量),由于是可变集,因此,可以正确执行myMutableSet += "Cloud Computing",不会报错。

注意:

虽然可变集和不可变集都有添加或删除元素的操作,但是,二者有很大的区别。

对不可变集进行操作,会产生一个新的集,原来的集并不会发生变化。

而对可变集进行操作,改变的是该集本身,

Map

在Scala中,映射(Map)是一系列键值对的集合,也就是,建立了键和值之间的对应关系。在映射中,所有的值,都可以通过键来获取。

映射包括可变和不可变两种,默认情况下创建的是不可变映射,如果需要创建可变映射,需要引入scala.collection.mutable.Map包。

下面我们创建一个不可变映射:

1
val university = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")

如果要获取映射中的值,可以通过键来获取,如下:

1
println(university("XMU"))

上面代码通过”XMU”这个键,可以获得值Xiamen University。
如果要检查映射中是否包含某个值,可以使用contains方法,如下:

1
2
val xmu = if (university.contains("XMU")) university("XMU") else 0
println(xmu)

循环遍历

1
2
3
for ((k,v) <- university) {
printf("Code is : %s and name is: %s\n",k,v)
}

我们只想把所有键打印出来:

1
for (k<-university.keys) println(k)

再比如说,我们只想把所有值打印出来:

1
for (v<-university.values) println(v)

filter

1
2
3
4
5
6
7
8
val university = Map(
"XMU" -> "Xiamen University",
"THU" -> "Tsinghua University",
"PKU" -> "Peking University",
"XMUT" -> "Xiamen University of Technology"
)
val universityOfXiamen = university filter { kv => kv._2 contains "Xiamen" }
println(universityOfXiamen)

结果

1
Map(XMU -> Xiamen University, XMUT -> Xiamen University of Technology)

Iterator

在Scala中,迭代器(Iterator)不是一个集合,但是,提供了访问集合的一种方法。

当构建一个集合需要很大的开销时(比如把一个文件的所有行都读取内存),迭代器就可以发挥很好的作用。
迭代器包含两个基本操作:next和hasNext。next可以返回迭代器的下一个元素,hasNext用于检测是否还有下一个元素。

有了这两个基本操作,我们就可以顺利地遍历迭代器中的所有元素了。
通常可以通过while循环或者for循环实现对迭代器的遍历。
while循环如下:

1
2
3
4
val iter = Iterator("Hadoop","Spark","Scala")
while (iter.hasNext) {
println(iter.next())
}

注意,上述操作执行结束后,迭代器会移动到末尾,就不能再使用了,如果继续执行一次println(iter.next)就会报错。另外,上面代码中,使用iter.nextiter.next()都是可以的,但是hasNext后面不能加括号。

for循环如下:

1
2
3
4
val iter = Iterator("Hadoop","Spark","Scala")
for (elem <- iter) {
println(elem)
}

遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
object Test {
def main(args: Array[String]): Unit = {
// 遍历List
val list = List(1, 2, 3)
for (item <- list) {
println("item:" + item)
}

//遍历Map
val user = Map("name" -> "小明", "age" -> 18)
println(user)
// 方式1
for ((k, v) <- user) {
printf("Key:%s Value:%s\n", k, v)
}
//方式2
user foreach {
case (k, v) => printf("Key:%s Value:%s\n", k, v)
}
}
}

模式匹配

Java中有switch-case语句,但是,只能按顺序匹配简单的数据类型和表达式。相对而言,Scala中的模式匹配的功能则要强大得多,可以应用到switch语句、类型检查、“解构”等多种场合。

简单匹配

Scala的模式匹配最常用于match语句中。下面是一个简单的整型值的匹配实例。

1
2
3
4
5
6
7
8
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命令提示符状态下执行下面命令:

1
scala test.scala

Shell 命令

另外,在模式匹配的case语句中,还可以使用变量。

1
2
3
4
5
6
7
8
val colorNum = 4
val colorStr = colorNum match {
case 1 => "red"
case 2 => "green"
case 3 => "yellow"
case unexpected => unexpected + " is Not Allowed"
}
println(colorStr)

按照前面给出的方法,在test.scala文件中测试执行上述代码后会在屏幕上输出:

1
4 is Not Allowed

也就是说,当colorNum=4时,值4会被传递给unexpected变量。

类型模式

Scala可以对表达式的类型进行匹配。

1
2
3
4
5
6
7
8
9
10
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文件中测试执行上述代码后会在屏幕上输出:

1
2
3
4
5
9 is an int value.
12.3 is a double value.
Spark is found.
Hadoop is a string value.
This is an unexpected value.

守卫语句

可以在模式匹配中添加一些必要的处理逻辑。

1
2
3
4
5
6
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
2
3
4
1 is odd.
2 is even.
3 is odd.
4 is even.

for表达式中的模式

我们之前在介绍“映射”的时候,实际上就已经接触过了for表达式中的模式。
还是以我们之前举过的映射为例子,我们创建的映射如下:

1
val university = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")

循环遍历映射的基本格式是:

1
for ((k,v) <- 映射) 语句块

对于遍历过程得到的每个值,都会被绑定到k和v两个变量上,也就是说,映射中的“键”被绑定到变量k上,映射中的“值”被绑定到变量v上。

下面给出此前已经介绍过的实例:

1
for ((k,v) <- university) printf("Code is : %s and name is: %s\n",k,v)

上面代码执行结果如下:

1
2
3
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类是一种特殊的类,它们经过优化以被用于模式匹配。

1
2
3
4
5
6
7
8
9
10
11
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”命令执行后可以得到如下结果:

1
2
3
Hello, BYD!
Hello, BMW!
Brand: Benz, Price:1500000, do you want it?

Option类型

标准类库中的Option类型用case类来表示那种可能存在、也可能不存在的值。
一般而言,对于每种语言来说,都会有一个关键字来表示一个对象引用的是“无”,在Java中使用的是null。

Scala融合了函数式编程风格,因此,当预计到变量或者函数返回值可能不会引用任何值的时候,建议你使用Option类型。

Option类包含一个子类Some,当存在可以被引用的值的时候,就可以使用Some来包含这个值,例如Some(“Hadoop”)。

而None则被声明为一个对象,而不是一个类,表示没有值。

下面我们给出一个实例。

1
2
3
4
5
6
7
8
9
val books = Map("hadoop" -> 5, "spark" -> 10, "hbase" -> 7)
val str_arr = Array("hadoop", "hive")
str_arr.foreach(item => {
val v = books.get(item)
v match {
case Some(value) => println("Some", value)
case None => println("None")
}
})

结果

1
2
(Some,5)
None

Option类型还提供了getOrElse方法,这个方法在这个Option是Some的实例时返回对应的值,而在是None的实例时返回传入的参数。例如:

1
2
3
4
5
6
val books = Map("hadoop" -> 5, "spark" -> 10, "hbase" -> 7)
val str_arr = Array("hadoop", "hive")
str_arr.foreach(f = item => {
val v = books.getOrElse(item, "No Such Book")
println(v)
})

结果

1
2
5
No Such Book

可以看出,当我们采用getOrElse方法时,如果我们取的hive没有对应的值,我们就可以显示我们指定的No Such Book,而不是显示None。
在Scala中,使用Option的情形是非常频繁的。

在Scala里,经常会用到Option[T]类型,其中的T可以是Sting或Int或其他各种数据类型。

Option[T]实际上就是一个容器,我们可以把它看做是一个集合,只不过这个集合中要么只包含一个元素(被包装在Some中返回),要么就不存在元素(返回None)。

既然是一个集合,我们当然可以对它使用map、foreach或者filter等方法。比如:

1
scala> books.get("hive").foreach(println)

可以发现,上述代码执行后,屏幕上什么都没有显示,因为,foreach遍历遇到None的时候,什么也不做,自然不会执行println操作。

类与对象

get/set

1
2
3
4
5
6
7
8
9
10
11
class User {
private var _age = 0;

// get方法
def age: Int = _age

// set方法
def age_=(a: Int): Unit = {
_age = a
}
}

测试

1
2
3
4
5
6
7
object Test {
def main(args: Array[String]): Unit = {
var u = new User()
u.age = 3
println(u.age)
}
}

构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User {
private var name = ""
private var age = 0

def this(name: String) {
this()
this.name = name
}

def this(name: String, age: Int) {
this(name)
this.age = age
}

def info(): Unit = {
printf("name:%s age is %d\n", name, age)
}
}

测试

1
2
3
4
5
6
object Test {
def main(args: Array[String]): Unit = {
var u = new User("小明", 18)
u.info()
}
}

单例

1
2
3
4
5
6
7
8
object IDGenerator {
private var id = 0;

def newID(): Int = {
id += 1
id
}
}

测试

1
2
3
4
5
6
object Test {
def main(args: Array[String]): Unit = {
println(IDGenerator.newID())
println(IDGenerator.newID())
}
}