Post

Scala基础教程

简介

Scala是一种结合了面向对象和函数式编程的、静态类型的高级编程语言。

Scala代码被编译成.class文件,运行在Java虚拟机(JVM)上,可以调用Java类库。

官方网站:https://www.scala-lang.org/

官方文档:https://docs.scala-lang.org/

官方教程:

在线运行环境:

sbt构建工具:https://www.scala-sbt.org/index.html

安装

第1步:安装Java

由于Scala运行在JVM上,因此首先要安装JDK并配置JAVA_HOME环境变量。可从Oracle JavaAdoptOpenJDK下载。

第2步:安装Scala

下载地址:https://www.scala-lang.org/download/all.html

要使用IDE编辑Scala,可安装IntelliJ IDEA的Scala插件或VSCode的Metals插件

第3步:配置环境变量

将SCALA_HOME环境变量设置为Scala安装/解压目录,并将$SCALA_HOME/bin目录添加到PATH环境变量。例如,在~/.bashrc文件末尾增加:

1
2
export SCALA_HOME=/usr/local/scala-2.13.8
export PATH=$PATH:$SCALA_HOME/bin

之后执行source ~/.bashrc或重启终端,即可使用scala和scalac命令:

1
2
3
4
$ scala -version
Scala code runner version 2.13.8 -- Copyright 2002-2021, LAMP/EPFL and Lightbend, Inc.
$ scalac -version
Scala compiler version 2.13.8 -- Copyright 2002-2021, LAMP/EPFL and Lightbend, Inc.

Hello, world

Scala的 “Hello, world” 程序如下:

1
2
3
4
5
object Hello {
  def main(args: Array[String]) = {
    println("Hello, world!")
  }
}

另一种写法:

1
2
3
object Hello extends App {
  println("Hello, world!")
}

将代码保存到Hello.scala文件,使用scalac命令编译:

1
$ scalac Hello.scala

该命令将生成两个文件:Hello.class和Hello$.class。这些和使用javac创建的字节码文件相同,可以在JVM上运行。使用scala命令运行Hello程序:

1
2
$ scala Hello
Hello, world!

Scala REPL

Scala REPL (“Read-Evaluate-Print-Loop”)是一个命令行解释器,可以将其用作playground来测试Scala代码(相当于Python的IDLE)。

在命令行输入scala即可启动REPL会话:

1
2
3
4
5
$ scala
Welcome to Scala 2.13.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_312).
Type in expressions for evaluation. Or try :help.

scala> 

输入:help查看帮助,输入:quit退出。

在REPL中可以输入Scala表达式:

1
2
3
4
5
scala> val x = 1
val x: Int = 1

scala> val y = x + 1
val y: Int = 2

两种类型的变量

Scala有两种类型的变量:

  • val是不可变变量,类似于Java中的final,应该是首选
  • var创建一个可变变量,只有在有特定原因时才应该使用它

例如:

1
2
val x = 1   // 不可变
var y = 0   // 可变

声明变量类型

在Scala中创建变量时通常不声明变量类型,Scala会自动推断类型:

1
2
3
val x = 1
val s = "a string"
val p = new Person("Regina")

这一特性称为类型推断,有助于保持代码简洁。也可以显式声明变量类型,但通常没有必要:

1
2
3
val x: Int = 1
val s: String = "a string"
val p: Person = new Person("Regina")

控制结构

if/else

Scala的if/else控制结构与其他语言类似:

1
2
3
4
5
6
7
8
9
if (test1) {
  doA()
} else if (test2) {
  doB()
} else if (test3) {
  doC()
} else {
  doD()
}

但是,与Java和其他语言不同的是,if/else结构返回一个值,因此可以将其用作三元运算符:

1
val x = if (a < b) a else b

match表达式

Scala的match表达式类似于Java的switch语句:

1
2
3
4
5
val result = i match {
  case 1 => "one"
  case 2 => "two"
  case _ => "not 1 or 2"
}

match表达式不仅限于整数,它可以与任何数据类型一起使用:

1
2
3
4
val booleanAsString = b match {
  case true => "true"
  case false => "false"
}

下面是一个将match用作方法体的示例,与多种不同的类型进行匹配:

1
2
3
4
5
6
7
8
def getClassAsString(x: Any): String = x match {
  case s: String => s + " is a String"
  case i: Int => "Int"
  case f: Float => "Float"
  case l: List[_] => "List"
  case p: Person => "Person"
  case _ => "Unknown"
}

(这里是判断x的类型而不是具体值,相当于Java 14的instanceof模式匹配和Java 17的switch模式匹配)

try/catch

Scala的try/catch控制结构用于捕获异常,类似于Java,但其语法与match表达式一致:

1
2
3
4
5
6
try {
  writeToFile(text)
} catch {
  case fnfe: FileNotFoundException => println(fnfe)
  case ioe: IOException => println(ioe)
}

for循环和表达式

Scala的for循环有三种形式:

1
2
3
4
5
6
7
8
// for-in
for (arg <- args) println(arg)

// "x to y" syntax
for (i <- 0 to 5) println(i)

// "x to y by" syntax
for (i <- 0 to 10 by 2) println(i)

注意:to后面的上界是包含的!

可以在for循环中添加yield关键字来创建for表达式(相当于Python的列表推导式)。下面是一个将序列1~5中的每个值加倍的for表达式:

1
2
scala> val x = for (i <- 1 to 5) yield i * 2
val x: IndexedSeq[Int] = Vector(2, 4, 6, 8, 10)

下面是一个迭代字符串列表的for表达式:

1
2
3
4
5
6
7
val fruits = List("apple", "banana", "lime", "orange")

val fruitLengths = for {
  f <- fruits
  if f.length > 4
} yield f.length
// val fruitLengths: List[Int] = List(5, 6, 6)

从字面上即可理解代码的含义。

while和do/while

Scala的whiledo/while循环语法如下:

1
2
3
4
5
6
7
8
9
// while loop
while (condition) {
  statement
}

// do-while
do {
  statement
} while (condition)

下面是一个Scala类的例子:

1
2
3
class Person(var firstName: String, var lastName: String) {
  def printFullName() = println(s"$firstName $lastName")
}

这是使用该类的方式:

1
2
3
4
val p = new Person("Julia", "Kern")
println(p.firstName)
p.lastName = "Manes"
p.printFullName()

注意,不需要创建 “get” 和 “set” 方法来访问类中的字段。

方法

像其他OOP语言一样,Scala类也有方法。Scala方法的语法如下:

1
2
def sum(a: Int, b: Int): Int = a + b
def concatenate(s1: String, s2: String): String = s1 + s2

不必声明方法的返回类型,因此这两个方法等价于

1
2
def sum(a: Int, b: Int) = a + b
def concatenate(s1: String, s2: String) = s1 + s2

这是调用方法的方式:

1
2
val x = sum(1, 2)
val y = concatenate("foo", "bar")

特质(Traits)

Scala的特质(traits)类似于Java的接口,可以将代码分解为小的模块化单元。例如,给定三个trait:

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Speaker {
  def speak(): String // has no body, so it’s abstract
}

trait TailWagger {
  def startTail(): Unit = println("tail is wagging")
  def stopTail(): Unit = println("tail is stopped")
}

trait Runner {
  def startRunning(): Unit = println("I’m running")
  def stopRunning(): Unit = println("Stopped running")
}

可以创建一个扩展(extend)所有这些trait的Dog类,同时实现speak方法:

1
2
3
class Dog(name: String) extends Speaker with TailWagger with Runner {
  def speak(): String = "Woof!"
}

类似地,Cat类覆盖了trait方法:

1
2
3
4
5
class Cat extends Speaker with TailWagger with Runner {
  def speak(): String = "Meow"
  override def startRunning(): Unit = println("Yeah ... I don’t run")
  override def stopRunning(): Unit = println("No need to stop")
}

集合类

虽然可以在Scala中使用Java集合类,但强烈建议学习基本的Scala集合类:ListListBufferVectorArrayBufferMapSet。Scala 集合类的一大好处是它们提供了许多强大的方法,可以使用这些方法来简化代码。

创建列表

Scala提供了很多创建列表的方法,例如:

1
2
3
4
5
6
7
8
9
10
11
scala> val nums = List.range(0, 10)
val nums: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> val nums = (1 to 10 by 2).toList
val nums: List[Int] = List(1, 3, 5, 7, 9)

scala> val letters = ('a' to 'f').toList
val letters: List[Char] = List(a, b, c, d, e, f)

scala> val letters = ('a' to 'f' by 2).toList
val letters: List[Char] = List(a, c, e)

序列方法

给定两个列表:

1
2
val nums = (1 to 10).toList
val names = List("joel", "ed", "chris", "maurice")

foreach方法:

1
2
3
4
5
scala> names.foreach(println)
joel
ed
chris
maurice

filter方法:

1
2
3
4
scala> nums.filter(_ < 4).foreach(println)
1
2
3

map方法:

1
2
3
4
5
6
7
8
scala> val doubles = nums.map(_ * 2)
val doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

scala> val capNames = names.map(_.capitalize)
val capNames: List[String] = List(Joel, Ed, Chris, Maurice)

scala> val lessThanFive = nums.map(_ < 5)
val lessThanFive: List[Boolean] = List(true, true, true, true, false, false, false, false, false, false)

foldLeft方法(相当于Python的functools.reduce()函数):

1
2
3
4
5
scala> nums.foldLeft(0)(_ + _)
val res18: Int = 55

scala> nums.foldLeft(1)(_ * _)
val res19: Int = 3628800

第一个例子求nums中所有数字的和,第二个例子求nums中所有数字的积。

详细介绍见Scala Book - Scala CollectionsOverview - Scala Collections

元组

元组可以将不同类型的元素放在一个小容器中。一个元组可以包含2~22个值,并且所有值都可以具有不同的类型。例如,下面是一个包含IntDoubleString三种类型的元组:

1
(11, 11.0, "Eleven")

其类型是Tuple3

元组在很多地方都很方便。例如,可以从方法中返回一个元组:

1
2
3
4
def getAaplInfo(): (String, BigDecimal, Long) = {
  // get the stock symbol, price, and volume
  ("AAPL", BigDecimal(123.45), 101202303L)
}

之后可以将方法的结果赋值给一个变量:

1
val t = getAaplInfo()

通过数字前面加下划线来访问元组的值:

1
2
3
4
5
6
7
8
scala> t._1
val res23: String = AAPL

scala> t._2
val res24: BigDecimal = 123.45

scala> t._3
val res25: Long = 101202303

下面的例子将元组中的字段赋值给变量(类似于Python的元组赋值):

1
2
3
4
scala> val (symbol, price, volume) = t
val symbol: String = AAPL
val price: BigDecimal = 123.45
val volume: Long = 101202303

元组非常适合快速、临时地将一些东西组合在一起。如果发现多次使用相同的元组,则声明一个专用的case类可能很有用。例如:

1
case class StockInfo(symbol: String, price: BigDecimal, volume: Long)
This post is licensed under CC BY 4.0 by the author.