Scala
概述
Scala是一门现代的多范式语言,志在以简洁、优雅及类型安全的方式来表达常用的编程模型。
它平滑地集成了面向对象和函数式语言的特性。
变量
使用val
或者var
来定义变量,语法格式如下:
val/var 变量标识:变量类型 = 初始值
其中
val
定义的是不可重新赋值的变量var
定义的是可重新赋值的变量
[注意!]
- scala中定义变量类型写在变量名后面
- scala的语句最后不需要添加分号、
数据类型
基础类型 | 类型说明 |
---|---|
Byte | 8位带符号整数 |
Short | 16位带符号整数 |
Int | 32位带符号整数 |
Long | 64位带符号整数 |
Char | 16位无符号Unicode字符 |
String | Char类型的序列(字符串) |
Float | 32位单精度浮点数 |
Double | 64位双精度浮点数 |
Boolean | true或false |
注意下 scala类型与Java的区别
[注意!]
- scala中所有的类型都使用大写字母开头
- 整形使用
Int
而不是Integer- scala中定义变量可以不写类型,让scala编译器自动推断
字符串
-
使用双引号
val/var 变量名 = “字符串”
-
使用插值表达式
val/var 变量名 = s"${变量/表达式}字符串"
-
使用三引号
val/var 变量名 = """字符串1 字符串2"""
类型层次结构
Any
是所有类型的超类型,也称为顶级类型。它定义了一些通用的方法如equals
、hashCode
和toString
。Any
有两个直接子类:AnyVal
和AnyRef
。
AnyVal
代表值类型。有9个预定义的非空的值类型分别是:Double
、Float
、Long
、Int
、Short
、Byte
、Char
、Unit
和Boolean
。Unit
是不带任何意义的值类型,它仅有一个实例可以像这样声明:()
。所有的函数必须有返回,所以说有时候Unit
也是有用的返回类型。
AnyRef
代表引用类型。所有非值类型都被定义为引用类型。在Scala中,每个用户自定义的类型都是AnyRef
的子类型。如果Scala被应用在Java的运行环境中,AnyRef
相当于java.lang.Object
。
类型转换
Nothing和Null
Nothing
是所有类型的子类型,也称为底部类型。没有一个值是Nothing
类型的。它的用途之一是给出非正常终止的信号,如抛出异常、程序退出或者一个无限循环(可以理解为它是一个不对值进行定义的表达式的类型,或者是一个不能正常返回的方法)。
Null
是所有引用类型的子类型(即AnyRef
的任意子类型)。它有一个单例值由关键字null
所定义。Null
主要是使得Scala满足和其他JVM语言的互操作性,但是几乎不应该在Scala代码中使用。我们将在后面的章节中介绍null
的替代方案。
数据结构
数组
定长数组
- 定长数组指的是数组的长度是不允许改变的
- 数组的元素是可以改变的
语法:
// 通过指定长度定义数组
val/var 变量名 = new Array[元素类型](数组长度)
// 用元素直接初始化数组
val/var 变量名 = Array(元素1, 元素2, 元素3...)
[!NOTE]
- 在scala中,数组的泛型使用
[]
来指定- 使用
()
来获取元素
变长数组
创建变长数组,需要提前导入ArrayBuffer类import scala.collection.mutable.ArrayBuffer
语法
-
创建空的ArrayBuffer变长数组,语法结构:
val/var a = ArrayBuffer[元素类型]()
-
创建带有初始元素的ArrayBuffer
val/var a = ArrayBuffer(元素1,元素2,元素3....)
数组的相关操作
- 使用
+=
添加元素 - 使用
-=
删除元素 - 使用
++=
追加一个数组到变长数组 - 使用for遍历数组
元组
元组可以用来包含一组不同类型的值。例如:姓名,年龄,性别,出生年月。元组的元素是不可变的。
语法:
使用括号来定义元组
val/var 元组 = (元素1, 元素2, 元素3....)
使用箭头来定义元组(元组只有两个元素)
val/var 元组 = 元素1->元素2
访问元组:
使用_1、_2、_3…来访问元组中的元素,_1表示访问第一个元素,依次类推
列表
不可变列表
语法:
使用List(元素1, 元素2, 元素3, ...)
来创建一个不可变列表,语法格式:
val/var 变量名 = List(元素1, 元素2, 元素3...)
使用Nil
创建一个不可变的空列表
val/var 变量名 = Nil
使用::
方法创建一个不可变列表
val/var 变量名 = 元素1 :: 元素2 :: Nil
[!TIP]
使用**::拼接方式来创建列表,必须在最后添加一个Nil
可变列表
可变列表就是列表的元素、长度都是可变的。
要使用可变列表,先要导入import scala.collection.mutable.ListBuffer
[!NOTE]
- 可变集合都在
mutable
包中- 不可变集合都在
immutable
包中(默认导入)
定义
使用ListBuffer[元素类型]()
创建空的可变列表,语法结构:
val/var 变量名 = ListBuffer[Int]()
使用ListBuffer(元素1, 元素2, 元素3...)
创建可变列表,语法结构:
val/var 变量名 = ListBuffer(元素1,元素2,元素3...)
可变列表的操作:
- 获取元素(使用括号访问
(索引值)
) - 添加元素(
+=
) - 追加一个列表(
++=
) - 更改元素(
使用括号获取元素,然后进行赋值
) - 删除元素(
-=
) - 转换为List(
toList
) - 转换为Array(
toArray
)
set
Set(集)是代表没有重复元素的集合。Set具备以下性质:
- 元素不重复
- 不保证插入顺序
scala中的集也分为两种,一种是不可变集,另一种是可变集。
不可变集
语法
创建一个空的不可变集,语法格式:
val/var 变量名 = Set[类型]()
给定元素来创建一个不可变集,语法格式:
val/var 变量名 = Set(元素1, 元素2, 元素3...)
可变集
定义
可变集合不可变集的创建方式一致,只不过需要提前导入一个可变集类。
手动导入:import scala.collection.mutable.Set
Map
Map可以称之为映射。它是由键值对组成的集合。在scala中,Map也分为不可变Map和可变Map。
不可变Map
语法
val/var map = Map(键->值, 键->值, 键->值...) // 推荐,可读性更好
val/var map = Map((键, 值), (键, 值), (键, 值), (键, 值)...)
可变Map
定义语法与不可变Map一致。但定义可变Map需要手动导入import scala.collection.mutable.Map
伴生对象
一个class和object具有同样的名字。这个object称为伴生对象,这个class称为伴生类
- 伴生对象必须要和伴生类一样的名字
- 伴生对象和伴生类在同一个scala源文件中
- 伴生对象和伴生类可以互相访问private属性
样例类
样例类是一种特殊类,它可以用来快速定义一个用于保存数据的类(类似于Java POJO类)
语法格式:
case class 样例类名(var/val 成员变量名1:类型1, 成员变量名2:类型2, 成员变量名3:类型3)
- 如果要实现某个成员变量可以被修改,可以添加var
- 默认为val,可以省略
- 注意在实例化样例类时,并没有使用关键字
new
,这是因为样例类有一个默认的apply
方法来负责对象的创建。 - 样例类在比较的时候是按值比较而非按引用比较
参考代码:
//定义一个Person样例类,包含姓名和年龄成员变量
//创建样例类的对象实例("张三"、20),并打印它
object CaseClassDemo {
case class Person(name:String, age:Int)
def main(args: Array[String]): Unit = {
val zhangsan = Person("张三", 20)
println(zhangsan)
}
}
高阶函数
强制转换方法为函数
你同样可以传入一个对象方法作为高阶函数的参数,这是因为Scala编译器会将方法强制转换为一个函数。
case class WeeklyWeatherForecast(temperatures: Seq[Double]) {
private def convertCtoF(temp: Double) = temp * 1.8 + 32
def forecastInFahrenheit: Seq[Double] = temperatures.map(convertCtoF) // <-- passing the method convertCtoF
}
在这个例子中,方法convertCtoF
被传入forecastInFahrenheit
。这是可以的,因为编译器强制将方法convertCtoF
转成了函数x => convertCtoF(x)
(注: x
是编译器生成的变量名,保证在其作用域是唯一的)。
接收函数作为参数的函数
使用高阶函数的一个原因是减少冗余的代码。比方说需要写几个方法以通过不同方式来提升员工工资,若不使用高阶函数,代码可能像这样:
object SalaryRaiser {
def smallPromotion(salaries: List[Double]): List[Double] =
salaries.map(salary => salary * 1.1)
def greatPromotion(salaries: List[Double]): List[Double] =
salaries.map(salary => salary * math.log(salary))
def hugePromotion(salaries: List[Double]): List[Double] =
salaries.map(salary => salary * salary)
}
注意这三个方法的差异仅仅是提升的比例不同,为了简化代码,其实可以把重复的代码提到一个高阶函数中:
object SalaryRaiser {
private def promotion(salaries: List[Double], promotionFunction: Double => Double): List[Double] =
salaries.map(promotionFunction)
def smallPromotion(salaries: List[Double]): List[Double] =
promotion(salaries, salary => salary * 1.1)
def bigPromotion(salaries: List[Double]): List[Double] =
promotion(salaries, salary => salary * math.log(salary))
def hugePromotion(salaries: List[Double]): List[Double] =
promotion(salaries, salary => salary * salary)
}
新的方法promotion
有两个参数,薪资列表和一个类型为Double => Double
的函数(参数和返回值类型均为Double),返回薪资提升的结果。
返回函数的函数
有一些情况你希望生成一个函数, 比如:
def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = {
val schema = if (ssl) "https://" else "http://"
(endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query"
}
val domainName = "www.example.com"
def getURL = urlBuilder(ssl=true, domainName)
val endpoint = "users"
val query = "id=1"
val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String
注意urlBuilder的返回类型是(String, String) => String
,这意味着返回的匿名函数有两个String参数,返回一个String。在这个例子中,返回的匿名函数是(endpoint: String, query: String) => s"https://www.example.com/$endpoint?$query"
。
多参数列表(柯里化)
方法可以定义多个参数列表,当使用较少的参数列表调用多参数列表的方法时,会产生一个新的函数,该函数接收剩余的参数列表作为其参数。这被称为柯里化。
下面是一个例子,在Scala集合 trait TraversableOnce
定义了 foldLeft
def foldLeft[B](z: B)(op: (B, A) => B): B
foldLeft
从左到右,以此将一个二元运算op
应用到初始值z
和该迭代器(traversable)的所有元素上。以下是该函数的一个用例:
从初值0开始, 这里 foldLeft
将函数 (m, n) => m + n
依次应用到列表中的每一个元素和之前累积的值上。
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val res = numbers.foldLeft(0)((m, n) => m + n)
print(res) // 55
多参数列表有更复杂的调用语法,因此应该谨慎使用,建议的使用场景包括:
单一的函数参数
在某些情况下存在单一的函数参数时,例如上述例子foldLeft
中的op
,多参数列表可以使得传递匿名函数作为参数的语法更为简洁。如果不使用多参数列表,代码可能像这样:
numbers.foldLeft(0, {(m: Int, n: Int) => m + n})
注意使用多参数列表时,我们还可以利用Scala的类型推断来让代码更加简洁(如下所示),而如果没有多参数列表,这是不可能的。
numbers.foldLeft(0)(_ + _)
像上述语句这样,我们可以给定多参数列表的一部分参数列表(如上述的z
)来形成一个新的函数(partially applied function),达到复用的目的,如下所示:
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val numberFunc = numbers.foldLeft(List[Int]())_
val squares = numberFunc((xs, x) => xs:+ x*x)
print(squares.toString()) // List(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)
val cubes = numberFunc((xs, x) => xs:+ x*x*x)
print(cubes.toString()) // List(1, 8, 27, 64, 125, 216, 343, 512, 729, 1000)
最后,foldLeft
和 foldRight
可以按以下任意一种形式使用,
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
numbers.foldLeft(0)((sum, item) => sum + item) // Generic Form
numbers.foldRight(0)((sum, item) => sum + item) // Generic Form
numbers.foldLeft(0)(_+_) // Curried Form
numbers.foldRight(0)(_+_) // Curried Form
隐式(implicit)参数
如果要指定参数列表中的某些参数为隐式(implicit),应该使用多参数列表。例如:
def execute(arg: Int)(implicit ec: ExecutionContext) = ???