Post

Scala基础教程 第1节 基础

Scala基础教程 第1节 基础

本文将对Scala语言的常用特性进行详细介绍。

参考:

https://docs.scala-lang.org/overviews/scala-book/preliminaries.html

Scala的安装和环境配置参见《Scala快速入门教程》。本文使用的Scala版本是2.13。

1.1 Hello, World

https://docs.scala-lang.org/overviews/scala-book/hello-world-1.html

下面是Scala “Hello, world” 示例的源代码:

1
2
3
4
5
object Hello {
  def main(args: Array[String]): Unit = {
    println("Hello, world")
  }
}
  • 这段代码在一个名为Helloobject中定义了一个名为main的方法。
  • object类似于class,但只有单个实例。这意味着main()类似于Java中的静态方法。
  • main()接受一个名为args的字符串数组参数。
  • Array是包装Java数组的类。

将源代码保存到名为Hello.scala的文件中,之后在命令行运行scalac命令进行编译(类似于javac):

1
$ scalac Hello.scala

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

使用scala命令运行Hello程序:

1
2
$ scala Hello
Hello, world

可以使用javap命令查看Hello.class文件:

1
2
3
4
5
$ javap Hello.class
Compiled from "Hello.scala"
public final class Hello {
  public static void main(java.lang.String[]);
}

可以看到,Scala创建的.class文件就像是从Java源代码创建的一样。Scala代码可以在JVM上运行,也可以使用现有的Java库,这对Scala程序员来说都是极大的优势。

1.2 Hello, World - 版本2

https://docs.scala-lang.org/overviews/scala-book/hello-world-2.html

Scala提供了一种更方便地编写应用程序的方式。可以让object扩展App特质(特质类似于Java的接口,将在第4节详细介绍),而不是定义main()方法,如下所示:

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

运行结果与上一节的程序相同。App特质有自己的main()方法,该方法执行子类的初始化代码(即类体中的代码,在这里是println()调用)。

命令行参数

可以通过main()方法的args参数访问命令行参数。如果扩展App,也可以直接访问args变量。

1
2
3
4
5
6
object HelloYou extends App {
  if (args.size == 0)
    println("Hello, you")
  else
    println("Hello, " + args(0))
}

编译这段代码,并分别带和不带命令行参数运行:

1
2
3
4
5
6
7
$ scalac HelloYou.scala

$ scala HelloYou
Hello, you

$ scala HelloYou Al
Hello, Al

args是一个数组,可以通过args.size(或args.length)获得元素个数,通过args(i)(不是[])访问元素。(注:与Java一样,在Scala中命令行参数不包括程序名)

1.3 Scala REPL

https://docs.scala-lang.org/overviews/scala-book/scala-repl.html

Scala REPL (“Read-Evaluate-Print-Loop”)是一个命令行解释器,可以用作playground来测试Scala代码。在命令行输入scala即可启动REPL会话:

1
2
3
4
5
$ scala
Welcome to Scala 2.13.14 (Java HotSpot(TM) 64-Bit Server VM, Java 17.0.12).
Type in expressions for evaluation. Or try :help.

scala> 

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

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

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

如果不把表达式的结果赋给变量,REPL会自动创建以res开头的变量:

1
2
3
4
5
6
7
8
scala> 2 + 3
val res0: Int = 5

scala> 8 / 4
val res1: Int = 2

scala> val z = res0 * res1
val z: Int = 10

按Tab键自动补全:

1
2
scala> "hello".ta
tail         tails        take(        takeRight(   takeWhile(   tapEach(

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

详见Scala REPL overview

1.4 两种类型的变量

https://docs.scala-lang.org/overviews/scala-book/two-types-variables.html

Scala有两种类型的变量:

  • val创建不可变变量
  • var创建可变变量

在Scala中像这样声明变量:

1
2
3
4
val s = "hello"   // immutable
var i = 42        // mutable

val p = new Person("Joel Fleischman")

编译器能够从=右侧的表达式推断出变量的类型。如果愿意,也可以显式声明变量类型:

1
2
val s: String = "hello"
var i: Int = 42

大多数情况下不需要显式类型,但如果能使代码更容易阅读也可以添加。另见类型推断

valvar的区别在于:val变量不可变,初始化后不能重新赋值(类似于Java中的final);而var变量可变,可以多次赋值。因此val变量也称为 “value” 而不是 “variable” 。

1
2
3
4
5
6
scala> val a = 'a'
val a: Char = a

scala> a = 'b'
         ^
       error: reassignment to val
1
2
3
4
5
scala> var a = 'a'
var a: Char = a

scala> a = 'b'
// mutated a

注:val变量不同于C++中的const,只是不能重新赋值,但可以调用修改类成员的方法。

一般规则是:除非有充分的理由,否则应该始终使用val

注意,在REPL中可以重新定义val变量,而在真实代码中不能这样做。

1
2
3
4
5
scala> val age = 18
val age: Int = 18

scala> val age = 19
val age: Int = 19

1.5 内置类型

https://docs.scala-lang.org/overviews/scala-book/built-in-types.html

1.5.1 数值类型

Scala具有标准数值类型。与Java不同,在Scala中这些数据类型都是对象(不是基本类型),例如整数类型是scala.Int

像这样声明数值类型的变量:

1
2
3
4
5
6
val b: Byte = 1
val s: Short = 2
val x: Int = 3
val l: Long = 4
val f: Float = 5.0
val d: Double = 6.0

如果不显式声明类型,整数字面值(如1)默认为Int,浮点数字面值(如2.0)默认为Double。后缀L表示Long字面值(如1L),后缀fF表示Float字面值(如2.0f),后缀dD表示Double字面值(可省略)。

1
2
3
4
val i = 123   // defaults to Int
val j = 123L  // Long
val x = 1.0   // defaults to Double
val y = 1.0f  // Float

数值数据类型及其范围如下表所示:

类型描述范围
Boolean布尔值truefalse
Byte8位有符号整数-128~127 (-27~27-1)
Short16位有符号整数-32768~32767 (-215~215-1)
Int32位有符号整数-2147483648~2147483647 (-231~231-1)
Long64位有符号整数-9223372036854775808~9223372036854775807 (-263~263-1)
Float32位IEEE 754单精度浮点数1.40129846432481707×10-45 ~ 3.40282346638528860×1038
Double64位IEEE 754双精度浮点数4.94065645841246544×10-324 ~ 1.79769313486231570×10308
Char16位无符号Unicode字符0~65535 (0~216-1)

每种类型的常量MinValueMaxValue表示最小值和最大值。

数值类型可以按以下方式转换:

type-casting-diagram

例如:

1
2
3
4
5
val x: Long = 987654321
val y: Float = x  // 9.8765434E8 (note that some precision is lost in this case)

val face: Char = '☺'
val number: Int = face  // 9786

转换是单向的。不能将一个Double赋给Int类型的变量,除非显式调用toInt

注:

  • Scala编译器会将这些数值类型翻译为对应的Java基本类型。例如,对于下面的Scala类
1
class Foo(val i: Int)

编译器生成的字节码等价于以下Java类:

1
2
3
4
5
6
7
public class Foo {
    private final int i;

    public int i() { return this.i; }

    public Foo(final int i) { this.i = i; }
}
  • Scala数值类型与对应的Java基本类型可以互相隐式转换,这些转换定义在scala.Predef类中。例如,scala.Intjava.lang.Integer的隐式转换定义如下:
1
2
implicit def int2Integer(x: Int): java.lang.Integer = x.asInstanceOf[java.lang.Integer]
implicit def Integer2int(x: java.lang.Integer): Int = x.asInstanceOf[Int]
  • 数值类型可以隐式转换为对应的Rich类(例如Int可转换为scala.runtime.RichInt),因此可以直接对数字调用方法:
1
2
scala> 1.to(5).toList
val res0: List[Int] = List(1, 2, 3, 4, 5)

1.5.2 BigInt和BigDecimal

对于大数字,Scala提供了BigIntBigDecimal,分别表示任意大的整数和任意精度的浮点数(注:底层分别使用java.math包中的BigIntegerBigDecimal实现)。

1
2
3
4
5
6
7
8
9
10
11
12
scala> val a = 12345678987654321
               ^
       error: integer number too large

scala> val b = BigInt("12345678987654321")
val b: scala.math.BigInt = 12345678987654321

scala> val c = 3.141592653589793238462643383279
val c: Double = 3.141592653589793

scala> val d = BigDecimal("3.141592653589793238462643383279")
val d: scala.math.BigDecimal = 3.141592653589793238462643383279

BigIntBigDecimal的一个优点是它们支持习惯的数值运算符(而不必像Java的大数值类一样只能使用方法调用):

1
2
3
4
5
scala> b + 1
val res0: scala.math.BigInt = 12345678987654322

scala> b * b
val res1: scala.math.BigInt = 152415789666209420210333789971041

1.5.3 String和Char

Scala也有StringChar类型,字面值分别用双引号和单引号括起来:

1
2
val name = "Bill"
val c = 'a'

1.6 字符串

https://docs.scala-lang.org/overviews/scala-book/two-notes-about-strings.html

Scala的字符串类就是java.lang.String,可以直接调用所有Java字符串方法。另外,Scala字符串可以隐式转换为StringOps,这个类提供了许多额外的辅助方法。例如:

1
2
3
4
5
6
7
8
9
10
11
scala> "hello, world".substring(7) // Java string method
val res0: String = world

scala> "hello, world".map(_.toUpper) // StringOps method
val res1: String = HELLO, WORLD

scala> "42".toInt // StringOps method
val res2: Int = 42

scala> "hello" * 3 // StringOps method
val res3: String = hellohellohello

1.6.1 字符串插值

Scala字符串有一种很好的特性,叫做字符串插值(string interpolation)。

字符串插值提供了一种在字符串中使用变量的方式。只需在字符串前添加前缀s,并在变量名前添加$。例如:

1
2
3
val name = "James"
val age = 30
println(s"$name is $age years old") // "James is 30 years old"

可以将变量名用花括号括起来:

1
println(s"${name} is ${age} years old")

也可以将表达式放在花括号中,例如:

1
2
scala> println(s"1+1 = ${1+1}")
1+1 = 2

带前缀f的字符串可以使用printf风格的格式化,变量名后面跟着像%d这样的格式字符串。例如:

1
2
3
val name = "James"
val height = 1.9
println(f"$name%s is $height%2.2f meters tall") // "James is 1.90 meters tall"

前缀raw类似于s,但它不执行转义字符。例如:

1
2
3
scala> val foo = 42
scala> println(raw"a\n$foo")
a\n42

另外,还可以自定义插值符,以及在模式匹配中使用字符串插值。详见文档String Interpolation

1.6.2 多行字符串

可以通过使用三个双引号来创建多行字符串:

1
2
3
val speech = """Four score and
               seven years ago
               our fathers ..."""

这种方式的一个缺点是第一行之后的行会被缩进:

1
2
3
4
scala> print(speech)
Four score and
               seven years ago
               our fathers ...

解决这个问题的一种简单方法是在第一行后之的所有行前添加一个|符号,并在字符串后调用stripMargin()方法:

1
2
3
val speech = """Four score and
               |seven years ago
               |our fathers ...""".stripMargin

这样所有行都是左对齐的:

1
2
3
4
scala> print(speech)
Four score and
seven years ago
our fathers ...

1.7 Scala类型层次结构

https://docs.scala-lang.org/tour/unified-types.html

在Scala中,所有值都有类型,包括数值和函数。下图展示了类型层次结构的一个子集。

Scala Type Hierarchy

  • Any是所有类型的超类型,也称为顶级类型(top type)。它定义了一些通用方法,例如equals()hashCode()toString()Any有两个直接子类:AnyValAnyRef
  • AnyVal表示值类型(value type)。有9种预定义的值类型,不可为null:Boolean, Byte, Short, Int, Long, Float, Double, CharUnitUnit是不包含有意义信息的值类型,只有单一实例,用字面值()表示。Unit可以用作无返回值的函数的返回类型(类似于Java的void和Python的None)。
  • AnyRef表示引用类型(reference type)。所有非值类型都被定义为引用类型。Scala中所有用户定义类型都是AnyRef的子类型。在Java运行时环境的上下文中,AnyRef对应于Java.lang.Object
  • Nothing是所有类型的子类型,也称为底部类型(bottom type)。没有类型为Nothing的值。通常用于“永远不会返回正常结果”的场景。例如,空列表的类型为List[Nothing];如果一个函数会抛出异常或进入无限循环,其返回类型可以定义为Nothing
  • Null是所有引用类型的子类型。它只有一个值,用关键字null表示。Null主要是为了与其他JVM语言互操作,不应该在Scala代码中使用。

1.8 元组

https://docs.scala-lang.org/overviews/scala-book/tuples.html

https://docs.scala-lang.org/tour/tuples.html

元组(tuple)可以将不同类型的元素放在同一个容器中。通过将元素放在圆括号中来创建元组。例如,

1
val ingredient = ("Sugar", 25)

创建了包含一个String元素和一个Int元素的2元组,其类型为(String, Int)(或者Tuple2[String, Int])。

元组可以包含2~22个元素,并且是不可变的。

当你只需要将一些东西组合在一起但不想定义一个类时,元组很有用。元组在从方法返回多个值时特别方便。例如:

1
2
3
4
def getStockInfo: (String, Double, Double) = {
  // other code here ...
  ("NFLX", 100.00, 101.00)  // this is a Tuple3
}

可以通过编号访问元组元素,如_1_2等。

1
2
println(ingredient._1) // Sugar
println(ingredient._2) // 25

也可以使用模式匹配将元组元素赋值给变量:

1
val (name, quantity) = ingredient

下面是另一个元组模式匹配的例子:

1
2
3
4
5
6
7
8
val planets = List(
  ("Mercury", 57.9), ("Venus", 108.2), ("Earth", 149.6),
  ("Mars", 227.9), ("Jupiter", 778.3))
planets.foreach {
  case ("Earth", distance) =>
    println(s"Our planet is $distance million kilometers from the sun")
  case _ =>
}

或者在for循环中:

1
2
3
4
val numPairs = List((2, 5), (3, -7), (20, 56))
for ((a, b) <- numPairs) {
  println(a * b)
}

1.9 命令行I/O

https://docs.scala-lang.org/overviews/scala-book/command-line-io.html

1.9.1 写输出

可以使用println()写到标准输出(stdout),并在末尾添加换行符:

1
println("Hello, world")

如果不希望添加换行符,则使用print()

1
print("Hello without newline")

使用printf()打印格式化输出:

1
printf("%s is %2.2f meters tall", "James", 1.9)

像这样写到标准错误(stderr):

1
System.err.println("yikes, an error happened")

1.9.2 读取输入

读取命令行输入最简单的方式是使用scala.io.StdIn类的readLine()方法。该方法从标准输入(stdin)读取一整行,并删除末尾的换行符,如果到达输入结尾则返回null

下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
import scala.io.StdIn.readLine

object HelloInteractive {
  def main(args: Array[String]): Unit = {
    print("Enter your first name: ")
    val firstName = readLine()

    print("Enter your last name: ")
    val lastName = readLine()

    println(s"Your name is $firstName $lastName")
  }
}
1
2
3
4
$ scala HelloInteractive  
Enter your first name: Alvin
Enter your last name: Alexander
Your name is Alvin Alexander
This post is licensed under CC BY 4.0 by the author.