1. 介绍

本文我们将学习 Scala,一门运行在 Java 虚拟机上的编程语言。Scala 设计初衷是要集成面向对象编程和函数式编程的各种特性,并兼容现有的Java程序。

首先我们将介绍Scala语言核心基础如值、变量、方法、流程控制等。然后探索一些高级功能,例如高阶函数、柯里化(Currying)、类、对象和模式匹配。

本教程适合具有一定编程经验的Java程序员,不适合0基础初学者。除了Scala还有其它运行在 JVM 上的编程语言,详情可以查看这篇文章

2. Maven 配置

在本教程中,我们将使用 Scala 官方安装包 https://www.scala-lang.org/download/

首先,在 pom.xml 中添加 Scala 标准库 scala-library :

<dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala-library</artifactId>
    <version>2.13.10</version>
</dependency>

然后,添加maven插件 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>

scala-langscala-maven-plugin 的最新版本,请查看Maven仓库。

3. 基础功能

本节我们使用 Scala 解释器(Scala interpreter)教学。

3.1. Interpreter

解释器是一个交互式命令行编程工具,类似于 Linux Shell。

下面我们用它打印 “hello world”:

C:\>scala
Welcome to Scala 2.13.10 (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' 启动解释器。

然后,我们在此提示符下输入表达式。解释器读取表达式,对其进行求值并打印结果。然后,它循环并再次显示提示。

由于它提供即时反馈,解释器是学习和测试编程语言的最简单方法。

3.2. 表达式

Any computable statement is an expression.

让我们写一些表达式,并观察结果:

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. Value 定义

scala 中将常量叫做 Value,使用 val 关键字声明。

scala> val pi:Double = 3.14
pi: Double = 3.14

scala> print(pi)
3.14

Value是不可变的,因此无法再次赋值:

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. 方法定义

Scala 使用 def 关键字定义方法。接着是方法名、参数声明、分隔符(冒号)和返回类型。之后是分隔符 (=),后面是方法主体。

与 Java 不同,我们不使用 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

如果方法不带参数,在定义和调用方法时可以省略括号。此外,如果方法体只有一个表达式,我们可以省略大括号

下面编写一个无参数方法 coinToss ,随机返回字符串 “Head” 或 “Tail”:

scala> def coinToss =  if (Math.random > 0.5) "Head" else "Tail"
coinToss: String

调用方法

scala> println(coinToss)
Tail
scala> println(coinToss)
Head

4. 流程控制

Control structures allow us to alter the flow of control in a program. We have the following control structures:

  • If-else 表达式
  • While 和 do while 循环
  • For 表达式
  • Try 表达式
  • Match 表达式

Unlike Java, we do not have continue or break keywords. We do have the return keyword. However, we should avoid using it.

Instead of the switch statement, we have Pattern Matching via match expression. Additionally, we can define our own control abstractions.

4.1. if-else

The if-else expression is similar to Java. The else part is optional. We can nest multiple if-else expressions.

Since it is an expression, it returns a value. Therefore, we use it similar to the ternary operator (?:) in Java. In fact, the language does not have have the ternary operator.

Using if-else, let’s write a method to compute the greatest common divisor:

def gcd(x: Int, y: Int): Int = {
  if (y == 0) x else gcd(y, x % y)
}

Then, let’s write a unit test for this method:

@Test
def whenGcdCalledWith15and27_then3 = {
  assertEquals(3, gcd(15, 27))
}

4.2. While 循环

The while loop has a condition and a body. It repeatedly evaluates the body in a loop while the condition is true – the condition is evaluated at the beginning of each iteration.

Since it has nothing useful to return, it returns Unit.

Let’s use the while loop to write a method to compute the greatest common divisor:

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
}

Then, let’s verify the result:

assertEquals(3, gcdIter(15, 27))

4.3. Do While 循环

The do while loop is similar to the while loop except that the loop condition is evaluated at the end of the loop.

Using the do-while loop, let’s write a method to compute factorial:

def factorial(a: Int): Int = {
  var result = 1
  var i = 1
  do {
    result *= i
    i = i + 1
  } while (i <= a)
  result
}

Next, let’s verify the result:

assertEquals(720, factorial(6))

4.4. for 循环

The for expression is much more versatile than the for loop in Java.

It can iterate over single or multiple collections. Moreover, it can filter out elements as well as produce new collections.

Using the for expression, let’s write a method to sum a range of integers:

def rangeSum(a: Int, b: Int) = {
  var sum = 0
  for (i <- a to b) {
    sum += i
  }
  sum
}

Here, a to b is a generator expression. It generates a series of values from a to b.

i <- a to b is a generator. It defines i as val and assigns it the series of values produced by the generator expression.

The body is executed for each value in the series.

Next, let’s verify the result:

assertEquals(55, rangeSum(1, 10))

5. 函数

Scala is a functional language. Functions are first-class values here – we can use them like any other value type.

In this section, we’ll look into some advanced concepts related to functions – local functions, higher-order functions, anonymous functions, and currying.

5.1. Local Functions

We can define functions inside functions. They are referred to as nested functions or local functions. Similar to the local variables, they are visible only within the function they are defined in.

Now, let’s write a method to compute power using a nested function:

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)
}

Next, let’s verify the result:

assertEquals(8, power(2, 3))

5.2. 高阶函数

Since functions are values, we can pass them as parameters to another function. We can also have a function return another function.

We refer to functions which operate on functions as higher-order functions. They enable us to work at a more abstract level. Using them, we can reduce code duplication by writing generalized algorithms.

Now, let’s write a higher-order function to perform a map and reduce operation over a range of integers:

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)
}

Here, r and m are parameters of Function type. By passing different functions, we can solve a range of problems, such as the sum of squares or cubes, and the factorial.

Next, let’s use this function to write another function sumSquares that sums the squares of integers:

@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))
}

Above, we can see that higher-order functions tend to create many small single-use functions. We can avoid naming them by using anonymous functions.

5.3. 匿名函数

An anonymous function is an expression that evaluates to a function. It is similar to the lambda expression in Java.

Let’s rewrite the previous example using anonymous functions:

@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))
}

In this example, mapReduce receives two anonymous functions: (x, y) => x + y and x => x * x.

Scala can deduce the parameter types from context. Therefore, we are omitting the type of parameters in these functions.

This results in a more concise code compared to the previous example.

5.4. Currying Functions

A curried function takes multiple argument lists, such as def f(x: Int) (y: Int). It is applied by passing multiple argument lists, as in f(5)(6).

It is evaluated as an invocation of a chain of functions. These intermediate functions take a single argument and return a function.

We can also partially specify argument lists, such as f(5).

Now, let’s understand this with an example:

@Test
def whenSumModCalledWith6And10_then10 = {
  // a curried function
  def sum(f : Int => Int)(a : Int, b : Int) : Int =
    if (a > b) 0 else f(a) + sum(f)(a + 1, b)

  // another curried function
  def mod(n : Int)(x : Int) = x % n

  // application of a curried function
  assertEquals(1, mod(5)(6))
    
  // partial application of curried function
  // trailing underscore is required to 
  // make function type explicit
  val sumMod5 = sum(mod(5)) _

  assertEquals(10, sumMod5(6, 10))
}

Above, sum and mod each take two argument lists.
We pass the two arguments lists like mod(5)(6). This is evaluated as two function calls. First, mod(5) is evaluated, which returns a function. This is, in turn, invoked with argument 6. We get 1 as the result.

It is possible to partially apply the parameters as in mod(5). We get a function as a result.

Similarly, in the expression sum(mod(5)) _, we are passing only the first argument to sum function. Therefore, sumMod5 is a function.

The underscore is used as a placeholder for unapplied arguments. Since the compiler cannot infer that a function type is expected, we are using the trailing underscore to make the function return type explicit.

5.5. By-Name Parameters

A function can apply parameters in two different ways – by value and by name – it evaluates by-value arguments only once at the time of invocation. In contrast, it evaluates by-name arguments whenever they are referred. If the by-name argument is not used, it is not evaluated.

Scala uses by-value parameters by default. If the parameter type is preceded by arrow ( =>), it switches to by-name parameter.

Now, let’s use it to implement the while loop:

def whileLoop(condition: => Boolean)(body: => Unit): Unit =
  if (condition) {
    body
    whileLoop(condition)(body)
  }

For the above function to work correctly, both parameters condition and body should be evaluated every time they are referred. Therefore, we are defining them as by-name parameters.

6. Class 定义

We define a class with the class keyword followed by the name of the class.

After the name, we can specify primary constructor parameters. Doing so automatically adds members with the same name to the class.

In the class body, we define the members – values, variables, methods, etc. They are public by default unless modified by the private or protected access modifiers.

We have to use the override keyword to override a method from the superclass.

Let’s define a class 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)"
}

Here, we are specifying three constructor parameters – name, salary, and annualIncrement.

Since we are declaring name and salary with val and var keywords, the corresponding members are public. On the other hand, we are not using val or var keyword for the annualIncrement parameter. Therefore, the corresponding member is private. As we are specifying a default value for this parameter, we can omit it while calling the constructor.

In addition to the fields, we are defining the method incrementSalary. This method is public.

Next, let’s write a unit test for this class:

@Test
def whenSalaryIncremented_thenCorrectSalary = {
  val employee = new Employee("John Doe", 1000)
  employee.incrementSalary()
  assertEquals(1020, employee.salary)
}

6.1. 抽象类

We use the keyword abstract to make a class abstract. It is similar to that in Java. It can have all the members that a regular class can have.

Furthermore, it can contain abstract members. These are members with just declaration and no definition, with their definition is provided in the subclass.

Similarly to Java, we cannot create an instance of an abstract class.

Now, let’s illustrate the abstract class with an example.

First, let’s create an abstract class IntSet to represent the set of integers:

abstract class IntSet {
  // add an element to the set
  def incl(x: Int): IntSet

  // whether an element belongs to the set
  def contains(x: Int): Boolean
}

Next, let’s create a concrete subclass EmptyIntSet to represent the empty set:

class EmptyIntSet extends IntSet {          
  def contains(x : Int) = false          
  def incl(x : Int) =          
  new NonEmptyIntSet(x, this)          
}

Then, another subclass NonEmptyIntSet represent the non-empty sets:

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)
    }
}

Finally, let’s write a unit test for NonEmptySet:

@Test
def givenSetOf1To10_whenContains11Called_thenFalse = {
  // Set up a set containing integers 1 to 10.
  val set1To10 = Range(1, 10)
    .foldLeft(new EmptyIntSet() : IntSet) {
        (x, y) => x incl y
    }

  assertFalse(set1To10 contains 11)
}

6.2. Traits(特质)

什么是 Traits ?乍一看是不是一头雾水,其实它对应于 Java 中的接口(Rust中也把接口叫Traits),但有以下区别:

  • able to extend from a class
  • can access superclass members
  • can have initializer statements

We define them as we define classes but using the trait keyword. Besides, they can have the same members as abstract classes except for constructor parameters. Furthermore, they are meant to be added to some other class as a mixin.

Now, let’s illustrate traits using an example.

First, let’s define a trait UpperCasePrinter to ensure the toString method returns a value in the upper case:

trait UpperCasePrinter {
  override def toString =
    super.toString.toUpperCase
}

Then, let’s test this trait by adding it to an Employee class:

@Test
def givenEmployeeWithTrait_whenToStringCalled_thenUpper = {
  val employee = new Employee("John Doe", 10) with UpperCasePrinter
  assertEquals("EMPLOYEE(NAME=JOHN DOE, SALARY=10)", employee.toString)
}

Classes, objects, and traits can inherit at most one class but any number of traits.

7. Object 定义

Objects are instances of a class. As we have seen in previous examples, we create objects from a class using the new keyword.

However, if a class can have only one instance, we need to prevent the creation of multiple instances. In Java, we use the Singleton pattern to achieve this.

For such cases, we have a concise syntax called object definition – similar to the class definition with one difference. Instead of using the class keyword, we use the object keyword. Doing so defines a class and lazily creates its sole instance.

We use object definitions to implement utility methods and singletons.

Let’s define a Utils object:

object Utils {
  def average(x: Double, y: Double) =
    (x + y) / 2
}

Here, we are defining the class Utils and also creating its only instance.

We refer to this sole instance using its name Utils. This instance is created the first time it is accessed.

We cannot create another instance of Utils using the new keyword.

Now, let’s write a unit test for the Utils object:

assertEquals(15.0, Utils.average(10, 20), 1e-5)

7.1. Companion Object and Companion Class

If a class and an object definition have the same name, we call them as companion class and companion object respectively. We need to define both in the same file. Companion objects can access private members from their companion class and vice versa.

Unlike Java, we do not have static members. Instead, we use companion objects to implement static members.

8. 模式匹配

Pattern matching matches an expression to a sequence of alternatives. Each of these begins with the keyword case. This is followed by a pattern, separator arrow (=>) and a number of expressions. The expression is evaluated if the pattern matches.

We can build patterns from:

  • case class constructors
  • variable pattern
  • the wildcard pattern _
  • literals
  • constant identifiers

Case classes make it easy to do pattern matching on objects. We add case keyword while defining a class to make it a case class.

Thus, Pattern matching is much more powerful than the switch statement in Java. For this reason, it is a widely used language feature.

Now, let’s write the Fibonacci method using pattern matching:

def fibonacci(n:Int) : Int = n match {
  case 0 | 1 => 1
  case x if x > 1 =>
    fibonacci (x-1) + fibonacci(x-2) 
}

Next, let’s write a unit test for this method:

assertEquals(13, fibonacci(6))

9. sbt 使用

sbt is the de facto build tool for Scala projects. In this section, let’s understand its basic usage and key concepts.

9.1. Setting Up the Project

We can use sbt to set up a Scala project and manage its lifecycle. After downloading and installing it, let’s set up a minimalistic Scala project, scala-demo.

Firstly, we must add the project settings, such as name, organization, version, and scalaVersion within the build.sbt file:

$ cat build.sbt
scalaVersion := "3.3.0"
version := "1.0"
name := "sbt-demo"
organization := "com.baeldung"

Now, when we run the sbt command from the project’s root directory, it starts the sbt shell:

scala-demo $ sbt
[info] Updated file /Users/tavasthi/baeldung/scala-demo/project/build.properties: set sbt.version to 1.8.3
[info] welcome to sbt 1.8.3 (Homebrew Java 20.0.1)
[info] loading global plugins from /Users/tavasthi/.sbt/1.0/plugins
[info] loading project definition from /Users/tavasthi/baeldung/scala-demo/project
[info] loading settings for project scala-demo from build.sbt ...
[info] set current project to sbt-demo (in build file:/Users/tavasthi/baeldung/scala-demo/)
[info] sbt server started at local:///Users/tavasthi/.sbt/1.0/server/de978fbf3c48749b6213/sock
[info] started sbt server
sbt:sbt-demo>

Additionally, it creates or updates the project/ and target/ directories required for managing the lifecycle of the project:

$ ls -ll
total 8
-rw-r--r--  1 tavasthi  staff   94 Jul 22 22:40 build.sbt
drwxr-xr-x  4 tavasthi  staff  128 Jul 22 22:40 project
drwxr-xr-x  5 tavasthi  staff  160 Jul 22 22:41 target

Lastly, by default, sbt follows a conventional approach to project structure with src/main and src/test directories containing the main source code and test source code, respectively. So, let’s set up the project structure using this convention:

$ mkdir -p src/{main,test}/scala/com/baeldung

We must note that we’ve created the directory hierarchy to keep our files based on the organization name we used in the build.sbt file.

9.2. 编译代码

Using the sbt shell, we can build our Scala project conveniently.

First, let’s add a Main.scala file to our source code for printing the “*Hello, world!*” text:

$ cat src/main/scala/com/baeldung/Main.scala
package com.baeldung

object Main {
    val helloWorldStr = "Hello, world!"
    def main(args: Array[String]): Unit = println(helloWorldStr)
}

Similarly, let’s also add the DemoTest.scala test file under the src/test/scala directory:

$ cat src/test/scala/com/baeldung/DemoTest.scala
package com.baeldung
import org.scalatest.funsuite.AnyFunSuite

class DemoTest extends AnyFunSuite {
  test("add two numbers") {
    assert(2 + 2 == 4)
  }
}

Now, we’re ready to compile our code using the sbt compile command:

$ sbt compile
[info] welcome to sbt 1.8.3 (Homebrew Java 20.0.1)
[info] loading global plugins from /Users/tavasthi/.sbt/1.0/plugins
[info] loading project definition from /Users/tavasthi/baeldung/scala-demo/project
[info] loading settings for project scala-demo from build.sbt ...
[info] set current project to sbt-demo (in build file:/Users/tavasthi/baeldung/scala-demo/)
[info] Executing in batch mode. For better performance use sbt's shell
[success] Total time: 0 s, completed Jul 23, 2023, 12:53:39 PM

Great! It looks like we’ve got this right.

9.3. 依赖管理

We can also manage the dependencies within our project using the sbt utility. For adding a package dependency, we need to update the libraryDependencies property in the build.sbt file.

For our use case, let’s add a dependency on the scalatest package which will be required for running our test code:

libraryDependencies ++= Seq(
  "org.scalatest" %% "scalatest" % "3.2.13" % Test
)

We must note that we’ve used the ++= operator to extend the configuration and % as a delimiter between organization, artifact, and version information. Further, we used the %% operator to inject the project’s Scala version and defined the scope for dependency using the Test keyword.

9.4. Running Main and Test Code

We can use the sbt utility to run both the main and test code within our project.

First, let’s use the sbt run command to run the main code:

$ sbt run
[info] welcome to sbt 1.8.3 (Homebrew Java 20.0.1)
[info] loading global plugins from /Users/tavasthi/.sbt/1.0/plugins
[info] loading project definition from /Users/tavasthi/baeldung/scala-demo/project
[info] loading settings for project scala-demo from build.sbt ...
[info] set current project to sbt-demo (in build file:/Users/tavasthi/baeldung/scala-demo/)
[info] running com.baeldung.Main
Hello, world!
[success] Total time: 0 s, completed Jul 23, 2023, 1:01:09 PM

Perfect! We can see our main code running successfully.

Now, let’s run the tests for our project using the sbt test command:

$ sbt test
[info] welcome to sbt 1.8.3 (Homebrew Java 20.0.1)
[info] loading global plugins from /Users/tavasthi/.sbt/1.0/plugins
[info] loading project definition from /Users/tavasthi/baeldung/scala-demo/project
[info] loading settings for project scala-demo from build.sbt ...
[info] set current project to sbt-demo (in build file:/Users/tavasthi/baeldung/scala-demo/)
[info] compiling 1 Scala source to /Users/tavasthi/baeldung/scala-demo/target/scala-3.3.0/test-classes ...
[info] DemoTest:
[info] - add two numbers
[info] Run completed in 136 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 2 s, completed Jul 23, 2023, 1:03:52 PM

We can notice that our test is running fine.

10. 总结

In this article, we looked at the Scala language and learned some of its key features. As we have seen, it provides excellent support for imperative, functional and object-oriented programming.

As usual, the full source code can be found over on GitHub.