1. 简介
在本教程中,我们将介绍 Scala —— 一种运行在 Java 虚拟机(JVM)上的主流编程语言。
我们将从核心语言特性开始,如值、变量、方法和控制结构。然后,深入探讨一些高级特性,例如高阶函数、柯里化、类、对象和模式匹配。
如果你想了解 JVM 上的其他语言,可以查看我们的 JVM 语言快速指南。
2. 项目配置
在本教程中,我们将使用标准的 Scala 安装包,可以从 https://www.scala-lang.org/download/ 获取。
首先,将 scala-library
依赖添加到你的 pom.xml
文件中。这个依赖提供了 Scala 的标准库:
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.12.7</version>
</dependency>
其次,添加 scala-maven-plugin 插件,用于编译、测试、运行和生成文档:
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.3.2</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
Maven 上可以找到最新的 scala-lang 和 scala-maven-plugin 版本。
最后,我们使用 JUnit 进行单元测试。
3. 基础特性
在本节中,我们将通过示例来了解 Scala 的基础语言特性。我们使用 Scala 解释器 来演示。
3.1. 解释器
解释器是一个交互式 shell,可以用来编写程序和表达式。
让我们用它打印 "Hello World!":
C:\>scala
Welcome to Scala 2.12.6 (Java HotSpot(TM)
64-Bit Server VM, Java 1.8.0_92).
Type in expressions for evaluation.
Or try :help.
scala> print("Hello World!")
Hello World!
scala>
上面我们通过命令行输入 scala
启动解释器,随后它会显示欢迎信息并等待输入。
在提示符下输入表达式,解释器会读取、计算并打印结果,然后再次显示提示符。
由于解释器提供了即时反馈,因此是学习 Scala 的最佳起点。接下来,我们将用它来探索表达式和各种定义。
3.2. 表达式
任何可计算的语句都是表达式。
让我们写几个表达式并观察结果:
scala> 123 + 321
res0: Int = 444
scala> 7 * 6
res1: Int = 42
scala> "Hello, " + "World"
res2: String = Hello, World
scala> "zipZAP" * 3
res3: String = zipZAPzipZAPzipZAP
scala> if (11 % 2 == 0) "even" else "odd"
res4: String = odd
可以看到,每个表达式都有一个值和一个类型。
如果表达式没有返回值,它会返回一个 Unit
类型的值。该类型只有一个值:()
, 类似于 Java 中的 void
。
3.3. 值定义
使用关键字 val
来声明值。
我们用它来命名表达式的结果:
scala> val pi:Double = 3.14
pi: Double = 3.14
scala> print(pi)
3.14
这样我们可以多次重用该值。
值是不可变的,因此不能重新赋值:
scala> pi = 3.1415
<console>:12: error: reassignment to val
pi = 3.1415
^
3.4. 变量定义
如果需要重新赋值,应使用变量。
使用关键字 var
来声明变量:
scala> var radius:Int=3
radius: Int = 3
3.5. 方法定义
使用关键字 def
来定义方法。语法如下:
def 方法名(参数: 类型): 返回类型 = {
// 方法体
}
与 Java 不同的是,Scala 中不需要使用 return
关键字返回结果。方法会自动返回最后一个表达式的值。
例如,定义一个计算两个数平均值的方法 avg
:
scala> def avg(x:Double, y:Double):Double = {
(x + y) / 2
}
avg: (x: Double, y: Double)Double
调用方法:
scala> avg(10,20)
res0: Double = 12.5
如果方法不带参数,定义和调用时可以省略括号。如果方法体只有一个表达式,也可以省略大括号。
例如,定义一个随机返回 "Head" 或 "Tail" 的方法:
scala> def coinToss = if (Math.random > 0.5) "Head" else "Tail"
coinToss: String
调用:
scala> println(coinToss)
Tail
scala> println(coinToss)
Head
4. 控制结构
控制结构用于改变程序的执行流程。Scala 提供了以下几种控制结构:
- if-else 表达式
- while 循环和 do-while 循环
- for 表达式
- try 表达式
- match 表达式
与 Java 不同的是,Scala 没有 continue
或 break
关键字,但保留了 return
(建议少用)。替代 switch
的是 match
表达式,还可以自定义控制抽象。
4.1. if-else
if-else
表达式类似于 Java。else
是可选的,支持嵌套。
由于它是表达式,它会返回一个值。因此,它的用法类似于 Java 中的三元运算符 ?:
。事实上,Scala 没有三元运算符。
用 if-else
实现一个计算最大公约数的方法:
def gcd(x: Int, y: Int): Int = {
if (y == 0) x else gcd(y, x % y)
}
单元测试:
@Test
def whenGcdCalledWith15and27_then3 = {
assertEquals(3, gcd(15, 27))
}
4.2. While 循环
while
循环由条件和循环体组成,只要条件为真,就会重复执行循环体。
由于它没有返回值,所以返回 Unit
。
使用 while
实现一个计算最大公约数的迭代版本:
def gcdIter(x: Int, y: Int): Int = {
var a = x
var b = y
while (b > 0) {
a = a % b
val t = a
a = b
b = t
}
a
}
测试:
assertEquals(3, gcdIter(15, 27))
4.3. Do While 循环
do-while
与 while
类似,但循环条件在循环体执行后判断。
使用 do-while
计算阶乘:
def factorial(a: Int): Int = {
var result = 1
var i = 1
do {
result *= i
i = i + 1
} while (i <= a)
result
}
测试:
assertEquals(720, factorial(6))
4.4. For 表达式
Scala 的 for
表达式比 Java 的 for
循环更强大。
✅ 可以遍历单个或多个集合
✅ 可以过滤元素
✅ 可以生成新集合
使用 for
表达式实现一个计算整数区间和的方法:
def rangeSum(a: Int, b: Int) = {
var sum = 0
for (i <- a to b) {
sum += i
}
sum
}
其中,a to b
是一个生成器表达式,生成从 a
到 b
的一系列值。
i <- a to b
是一个生成器,它将每个值赋给 i
(i
是一个 val
)。
测试:
assertEquals(55, rangeSum(1, 10))
5. 函数
Scala 是一门函数式语言,函数是“一等公民”——可以像其他值一样使用。
本节将介绍函数的高级特性:局部函数、高阶函数、匿名函数和柯里化。
5.1. 局部函数
函数可以嵌套定义。这些函数称为嵌套函数或局部函数。与局部变量类似,它们只能在定义的函数内部访问。
例如,使用嵌套函数实现幂运算:
def power(x: Int, y:Int): Int = {
def powNested(i: Int,
accumulator: Int): Int = {
if (i <= 0) accumulator
else powNested(i - 1, x * accumulator)
}
powNested(y, 1)
}
测试:
assertEquals(8, power(2, 3))
5.2. 高阶函数
函数是值,因此可以作为参数传递给其他函数,也可以作为返回值。
操作函数的函数称为高阶函数。它们可以帮助我们编写更抽象、更通用的算法。
例如,实现一个通用的 map-reduce 函数:
def mapReduce(r: (Int, Int) => Int,
i: Int,
m: Int => Int,
a: Int, b: Int) = {
def iter(a: Int, result: Int): Int = {
if (a > b) {
result
} else {
iter(a + 1, r(m(a), result))
}
}
iter(a, i)
}
使用这个函数实现一个求平方和的方法:
@Test
def whenCalledWithSumAndSquare_thenCorrectValue = {
def square(x: Int) = x * x
def sum(x: Int, y: Int) = x + y
def sumSquares(a: Int, b: Int) =
mapReduce(sum, 0, square, a, b)
assertEquals(385, sumSquares(1, 10))
}
✅ 高阶函数往往会创建很多小的、一次性使用的函数。为避免命名,可以使用匿名函数。
5.3. 匿名函数
匿名函数是一个返回函数的表达式,类似于 Java 中的 lambda 表达式。
用匿名函数重写上面的例子:
@Test
def whenCalledWithAnonymousFunctions_thenCorrectValue = {
def sumSquares(a: Int, b: Int) =
mapReduce((x, y) => x + y, 0, x => x * x, a, b)
assertEquals(385, sumSquares(1, 10))
}
Scala 可以根据上下文推断参数类型,因此我们可以省略类型声明,使代码更简洁。
5.4. 柯里化函数
柯里化函数接受多个参数列表,例如:def f(x: Int)(y: Int)
。
调用时也需分组传参,如:f(5)(6)
。
它是通过链式函数调用来实现的,中间函数接受一个参数并返回另一个函数。
也可以部分应用参数,例如:f(5)
。
示例:
@Test
def whenSumModCalledWith6And10_then10 = {
// 柯里化函数
def sum(f : Int => Int)(a : Int, b : Int) : Int =
if (a > b) 0 else f(a) + sum(f)(a + 1, b)
// 另一个柯里化函数
def mod(n : Int)(x : Int) = x % n
// 应用柯里化函数
assertEquals(1, mod(5)(6))
// 部分应用
val sumMod5 = sum(mod(5)) _
assertEquals(10, sumMod5(6, 10))
}
✅ 使用 _
作为占位符表示未应用的参数。
5.5. 按名称传参
函数可以按值或按名称传递参数:
- 按值参数在调用时计算一次
- 按名称参数在每次引用时计算
使用 =>
标记按名称参数:
def whileLoop(condition: => Boolean)(body: => Unit): Unit =
if (condition) {
body
whileLoop(condition)(body)
}
✅ 适用于需要延迟求值的场景。
6. 类定义
使用 class
关键字定义类。
在类名后可以指定主构造函数参数。Scala 会自动为这些参数创建成员变量。
类中的成员默认是 public
,除非使用 private
或 protected
修饰。
使用 override
关键字覆盖父类方法。
示例:定义一个 Employee
类:
class Employee(val name : String, var salary : Int, annualIncrement : Int = 20) {
def incrementSalary() : Unit = {
salary += annualIncrement
}
override def toString =
s"Employee(name=$name, salary=$salary)"
}
测试:
@Test
def whenSalaryIncremented_thenCorrectSalary = {
val employee = new Employee("John Doe", 1000)
employee.incrementSalary()
assertEquals(1020, employee.salary)
}
6.1. 抽象类
使用 abstract
关键字定义抽象类。
抽象类可以包含抽象成员(只有声明没有实现),由子类提供实现。
示例:定义一个表示整数集合的抽象类 IntSet
:
abstract class IntSet {
def incl(x: Int): IntSet
def contains(x: Int): Boolean
}
实现两个子类:
class EmptyIntSet extends IntSet {
def contains(x : Int) = false
def incl(x : Int) =
new NonEmptyIntSet(x, this)
}
class NonEmptyIntSet(val head : Int, val tail : IntSet)
extends IntSet {
def contains(x : Int) =
head == x || (tail contains x)
def incl(x : Int) =
if (this contains x) {
this
} else {
new NonEmptyIntSet(x, this)
}
}
测试:
@Test
def givenSetOf1To10_whenContains11Called_thenFalse = {
val set1To10 = Range(1, 10)
.foldLeft(new EmptyIntSet() : IntSet) {
(x, y) => x incl y
}
assertFalse(set1To10 contains 11)
}
6.2. 特质(Traits)
特质类似于 Java 的接口,但更强大:
✅ 可以继承类
✅ 可以访问父类成员
✅ 可以有初始化语句
使用 trait
关键字定义特质。
示例:定义一个 UpperCasePrinter
特质:
trait UpperCasePrinter {
override def toString =
super.toString toUpperCase
}
使用:
@Test
def givenEmployeeWithTrait_whenToStringCalled_thenUpper = {
val employee = new Employee("John Doe", 10) with UpperCasePrinter
assertEquals("EMPLOYEE(NAME=JOHN DOE, SALARY=10)", employee.toString)
}
✅ 一个类最多继承一个类,但可以混入多个特质。
7. 对象定义
对象是类的实例。使用 new
关键字创建对象。
如果一个类只需要一个实例,可以使用对象定义。
使用 object
关键字定义单例对象。
示例:定义一个工具类 Utils
:
object Utils {
def average(x: Double, y: Double) =
(x + y) / 2
}
调用:
assertEquals(15.0, Utils.average(10, 20), 1e-5)
7.1. 伴生对象与伴生类
如果类和对象同名,它们互为伴生对象和伴生类。
✅ 必须定义在同一文件中
✅ 伴生对象可以访问伴生类的私有成员,反之亦然
✅ 用于替代 Java 的静态成员
8. 模式匹配
模式匹配用于将表达式与多个模式进行匹配。
每个模式以 case
开头,后跟模式、箭头 =>
和表达式。
模式可以包括:
- case 类构造器
- 变量模式
- 通配符
_
- 字面量
- 常量标识符
使用 case
关键字定义 case 类,便于模式匹配。
示例:使用模式匹配实现斐波那契数列:
def fibonacci(n:Int) : Int = n match {
case 0 | 1 => 1
case x if x > 1 =>
fibonacci (x-1) + fibonacci(x-2)
}
测试:
assertEquals(13, fibonacci(6))
9. 总结
在本教程中,我们介绍了 Scala 语言及其核心特性。Scala 同时支持面向对象、函数式和命令式编程风格。
完整代码可从 GitHub 获取。