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-langscala-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 没有 continuebreak 关键字,但保留了 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-whilewhile 类似,但循环条件在循环体执行后判断。

使用 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 是一个生成器表达式,生成从 ab 的一系列值。

i <- a to b 是一个生成器,它将每个值赋给 ii 是一个 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,除非使用 privateprotected 修饰。

使用 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 获取。


原始标题:Introduction to Scala