Scala学习笔记-入门(2)-Scala基础语法

注释

Scala中的注释和Java中的完全一样,具有单行注释、多行注释和文档注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
单行注释
//...

多行注释 不允许嵌套
/*...
...
*/

文档注释 注释内容可以被JDK提供的工具javadoc所解析,生成一套一网页文件形式体现的该程序的说明文档,一般写在类

/**
* @author:xxx
* @version:1.0
*/

变量和常量

在Java中,变量和常量语法如下:

1
2
3
4
5
// 变量
int a = 10;

// 常量
final int b = 20;

在Scala中,变量和常量的语法如下:

1
2
3
4
5
// 变量[variable]
var a: Int = 10

// 常量[value]
val b: Int = 20

在Scala中,变量和常量有如下的特点:

  • 声明变量的时候,类型可以省略,编译器会自动推导,即类型推导

  • 类型确定之后,就不能修改,该变量的类型不再改变

  • 变量声明的时候,必须要有初始值

  • 常量使用val声明,常量特性与Java中使用final关键字修饰的常量一致

  • 引用类型的常量,不能改变常量指向的对象,但是可以改变对象的字段

  • 建议:能够使用常量的地方就使用常量而不使用变量

关于强数据类型、弱数据类型、动态类型和静态类型:

  • 强弱数据类型指的是语言对类型检查的严格程度:弱类型相对于强类型来说类型检查更不严格,会允许一些变量类型的隐式转换等
  • 静态类型指编译器在编译阶段进行类型检查,动态类型指在执行过程中进行类型检查

标识符的命名规范

在Scala中,标识符的命名规范比Java中多出了两条,具体命名规则如下:

  1. 以字母或者下划线开头,后面可以接字母、数字、下划线(这点和和Java是一样的)
  2. 可以以操作符开头,但是必须只包含操作符(后续会看到这样做能够导致超级灵活的运算符重载)
  3. 使用反引号包括的任意字符串都可以作为标识符,即使是Scala中的关键字也可以

Scala中的关键字:

1
2
3
4
5
6
7
8
package	import		class		object	trait	extends		with	type	for
private protected abstract sealed final implicit lazy override
try catch finally throw
if else match case do while return yield
def val var
this super
new
true false null

其中Java没有的关键字:object trait with implicit match yield def val var

字符串输出

字符串的输出有三种基本方式:

  • 通过+号进行连接
  • 利用printf输出模板字符串:通过%占位
  • 使用插值字符串,通过${}来获取变量值,插值表达式中可以进行变量的运算

以上三种方式可以将变量值与字符串进行拼接,同时可以综合使用单行和多行的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val words: String = "HaHa"
val age: Int = 18

// 使用 + 拼接
println("I say " + words + " I am " + age)

// 可以使用 * 重复多个字符串
println(words * 4)

// 使用字符串模板
printf("I say %s I am %d", words, age)
println()

// 使用插值字符串,使用s开头
println(s"I say ${words} I am ${age + 2}")

在scala中提供的两种特殊字符串,s字符串和raw字符串。在s字符串中,可以使用插值表达式。而raw字符串与s字符串类似,同样可以使用插值表达式,只不过raw字符串不会进行转义。

1
2
3
val num: Double = 1.23456
println(s"num = ${num} \n 1")
println(raw"num = ${num} \n 1")

对应输出如下:

1
2
3
num = 1.23456
1
num = 1.23456 \n 1

还可以使用多行字符串"""...""",获得更加格式化的输出:

1
2
3
4
5
6
7
8
9
10
11
val name: String = "xxx"
val age: Int = 18
val s =
s"""
|select
| name,
| age
|from user
| where name = ${name} and age = ${age + 2}
|""".stripMargin
println(s)

其中|表示每一行的开头,.stripMargin为一个方法调用,输出结果如下,注意第一行的空行对应上面代码的第四行:

1
2
3
4
5
6

select
name,
age
from user
where name = xxx and age = 20

输入

键盘输入

基本语法,利用StdIn提供的方法,例如StdIn.readLine()StdIn.readShort()StdIn.readDounle()等,需要引入包scala.io.StdIn

文件输入和输出

1
2
3
4
5
6
7
8
9
10
import java.io.File, 
import java.io.PrintWriter
import scala.io.Source
// 读取文件
Source.fromFile("xxx").foreach(print)

// 写入文件
val writer = new PrintWriter(new File("xxxx"))
writer.write("content")
writer.close()

数据类型

数据类型概览

在Java中,数据类型分为基本数据类型和引用类型,基本数据类型对应还有包装类。由于基本数据类型的存在,Java可以认为并不是完全意义上的面向对象,Java中基本类型和引用类型并没有共同的祖先,包装类和引用数据类型的共同祖先才是Object类

在Scala中,一切数据都是对象。

  • 一切数据都是对象,都是Any的子类
  • 在Scala中数据类型分为两大类:数值类型(AnyVal)和引用类型(AnyRef),但是两者都是对象
  • 图中,实线表示继承关系,而虚线表示隐式的自动类型转换
  • String属于引用类型(AnyRef),而在Scala中设置了一个StringOps,属于数值类型,是对Java中String的增强
  • Unit属于AnyVal中的空值、Null属于AnyRef中的空值,是所有AnyRef的子类
  • Noting是所有数据类型的子类

整数类型

  • Byte:1字节
  • Short:2字节
  • Int:4字节(不同于Java中的Integer)
  • Long:8字节

整数默认为Int

浮点类型

  • Float:4字节
  • Double:8字节

浮点数默认为Double

字符类型

  • Char:2字节

布尔类型

  • Boolean:1字节,允许取值true和false

空类型

数据类型 描述
Unit 表示没有值,等同于void,用作不返回任何结果的方法和结果类型。Unit只有一个实例值,输出成()
Null 表示空引用,只有一个实例null
Nothing Nothing类型是其他任何类型的子类型,当一个函数确定没有正常的返回值,如抛出异常等,就可以利用Noting来指定返回类型
  • Unit
1
2
3
4
5
6
def doNoting(): Unit = {
println("Do Nothing")
}

val unit: Unit = doNoting()
println(unit)

输出如下

1
2
Do Nothing
()
  • Null
1
2
3
4
var str = new String("haha")
str = null // 正确

var n: Int = null // 错误
  • Nothing
1
2
3
def test: Nothing = {
throw new Exception()
}

类型转换

当Scala程序在进行赋值或者运算的时候,精度小的类型会自动转换成精度大的数值类型,这就是自动类型转换(隐式转换)。与Java中类似。

  • 自动类型提升:多种数据类型混合运算,自动提升到精度最大的数据类型。
  • 高精度赋值到低精度,直接报错。
  • 除了图中的隐式类型转换,都需要强制类型转换。
  • Byte Short Char计算时会直接提升为Int
  • Boolean不能参与整数浮点运算,不能隐式转换为整数。

强制类型转换

强制类型将数据由高精度转换为低精度,可能造成精度降低或者溢出。

强制类型转换利用到.toXXX,实际上也是一个方法调用的形式,此时的数值均为对象。

1
2
3
4
5
6
7
8
9
10
11
12
// 数值类型转换
var n1: Double = 5.4321
var n2: Int = n1.toInt

// 数值类型转化为String
val s1 = n1.toString
val s2 = n2.toString
val s3 = n1 + ""

// String转化为数值类型
val n3 = s1.toDouble
val n4 = s2.toInt

运算符

基本运算符

Scala中的运算符与Java基本相同,只有个别在细节上存在不同

  • 算术运算符:+ - * / %

    *可以利用在字符串重复上

  • 关系运算:== != < > <= >=

    ==的测试:

    • 在Java中,==可以比较两个基本数据类型的值,也可以比较两个引用类型的地址
    • 在Scala中,==相当于equals的调用,比较的更多是值(看对应的equals的实现)
    • 如果需要比较地址的话,需要使用eq

    测试代码(注意这里的new):

    1
    2
    3
    4
    5
    6
    val s1: String = "123"
    val s2: String = new String("123")

    println(s1==s2)
    println(s1.equals(s2))
    println(s1.eq(s2))

    输出如下:

    1
    2
    3
    true
    true
    false
  • 逻辑运算:&& || !,支持短路运算

  • 赋值运算:= += -= *= /= %= <<= >>= &= ^= !=

    注意:在Scala中没有++ --操作符,相关功能使用+= -=来实现

  • 位运算:& | ^ ~ << >> >>>

运算符的本质

在Scala中其实是没有运算符的,所有的运算符都是方法调用

  • 当调用对象的方法时,.可以省略
  • 如果函数参数只有一个或者没有的时候,()可以省略
1
2
3
4
5
val n: Int = 1
// 标准的加法运算
println(n.+(1))
println(n + (1))
println(n + 1)

在Scala中还有一个非常常用的运算符:=。这个运算符并不是内置的运算符,通常使用者会将它进行重载,将其定义为任何他们喜欢的意思。这个符号在其他语言中经常被用作赋值运算符。

流程控制

分支控制

if- else if -else 代码块,使用方式基本与Java相同

  • 与Java不同之处,Scala中if-else语句有返回值,返回值定义为执行到的最后一个语句的返回值
  • 可以强制要求返回Unit类型,此时忽略最后一个表达式的值,直接返回Unit
  • Scala中没有三元条件运算符?:,可以直接使用一行if else来完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
val n: Int = 18
// if-else语句返回值
val s1 = if (n > 10) {
println("into if")
"IF"
} else {
println("into else")
"ELSE"
}
println("s1=" + s1)
println("======")

// 强制返回Unit
val s2: Unit = if (n > 10) {
println("into if")
"IF"
} else {
println("into else")
"ELSE"
}
println("s2=" + s2)
println("======")

// if-else代替?:
val a: Int = 1
val b: Int = 2
val max = if (a > b) a else b
println(s"max of ${a} and ${b} is ${max}")

输出为:

1
2
3
4
5
6
7
into if
s1=IF
======
into if
s2=()
======
max of 1 and 2 is 2

Scala中没有Switch,而是使用模式匹配来处理,这部分会在后面进行讲解

循环控制

For循环

Scala中为for循环这一常见的控制结构提供了非常多的特性,这些for循环的特性被成为for推导式

  • 范围数据循环

    范围数据循环可以对比Python中的Range,下面的循环背后是得到了一个Range对象,每次给循环变量赋值对应的值。可以指定是否包含对应的右边界,可以指定步长

    to:包含右边界

    1
    2
    3
    4
    for (i <- 1 to 10) {
    print(i)
    }
    // 结果为:12345678910

    until:不包含右边界

    1
    2
    3
    4
    for (i <- 1 until  10) {
    print(i)
    }
    // 结果为:123456789

    上面的代码等价于

    1
    2
    3
    for (i <- new Range(1, 10, 1)) {
    print(i)
    }
  • 循环步长

    在循环中可以指定步长,通过by(步长不能为0)

    1
    2
    3
    4
    for (i <- 1 to 10 by 2) {
    print(i)
    }
    // 结果为:13579

    可以指定负数步长,利用负数步长可以完成逆序输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    for (i <- 1 to 10 by -1) {
    print(i)
    }
    // 结果为:(空,啥也没有)

    for (i <- 10 to 1 by -1) {
    print(i)
    }
    // 结果为:10987654321

    也可以利用reverse指定逆序输出

    1
    2
    3
    4
    for (i <- 1 to 10 by 2 reverse) {
    print(i)
    }
    // 结果为:97531

    步长可以设置为浮点数,但是需要指定循环范围的类型

    1
    2
    3
    4
    for(i <- 1.0 to 5.0 by 0.5){
    print(i + " ")
    }
    // 结果为:1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0
  • 循环守卫

    循环守卫,即循环保护式,也称条件判断式,只有满足条件才进入循环体内部,否则不进入。相当于在进入循环之前就进行判断,类似于continue

    1
    2
    3
    4
    for (i <- 1 to 5 if i != 2) {
    print(i)
    }
    // 结果为:1345
  • 嵌套循环

    嵌套循环可以类似于Java中嵌套实现,也可以将嵌套合并到一个for中

    1
    2
    3
    4
    5
    6
    for (i <- 1 to 5) {
    for (j <- 1 to 5) {
    print(i + j + " ")
    }
    }
    // 结果为:2 3 4 5 6 3 4 5 6 7 4 5 6 7 8 5 6 7 8 9 6 7 8 9 10

    等价于

    1
    2
    3
    for (i <- 1 to 5; j <- 1 to 5) {
    print(i + j + " ")
    }
  • 引入变量

    可以在for推导式中引入变量

    • for推导式中一行有多个表达式时,使用;来隔断逻辑
    • for推导式中,当只包含单一表达式的时候使用圆括号(),当有多个条件的时候使用花括号{},并且每一行一个表达式
    1
    2
    3
    4
    for (i <- 1 to 5; j = 5 - i) {
    print(j + " ")
    }
    // 结果为:4 3 2 1 0

    等价于

    1
    2
    3
    4
    5
    6
    for {
    i <- 1 to 5
    j = 5 - i
    } {
    print(j + " ")
    }
  • 循环返回值

    单独循环表达式的返回值是Unit,但是可以使用yield来在循环中返回,可以类比Python中的列表推导(注意:与Python中的yield不同)。在循环中使用yield,会将每次循环的结果加入一个List中,循环完毕之后再返回。

    1
    2
    3
    4
    val res = for (i <- 1 to 5) {
    }
    print(res)
    // 结果为:()
    1
    2
    3
    val res = for (i <- 1 to 5) yield i
    print(res)
    // 结果为:Vector(1, 2, 3, 4, 5)

While循环

whiledo while

  • 这两个关键字的用法与Java中是一模一样的,结果类型是Unit
  • 这两个关键字是为了保持对Java的兼容,在Scala中不推荐使用

循环中断

Scala内置控制结果中特地去除了breakcontinue关键字,为了更好地适应函数式编程。在Scala中推荐使用函数式的风格来解决break和continue的功能。

  • 实现break

采用异常方式退出循环:

1
2
3
4
5
6
7
8
9
10
11
try {
for (i <- 1 to 10) {
print(i + " ")
if (i == 5) throw new RuntimeException()
}
}
catch {
case e =>
}
println("执行循环体后的代码")
// 结果为:1 2 3 4 5 执行循环体后的代码

采用Scala自带的函数退出循环(底层还是使用的try-catch的解决方案):

1
2
3
4
5
6
7
8
9
import scala.util.control.Breaks

Breaks.breakable(
for (i <- 1 to 10) {
print(i + " ")
if (i == 5) Breaks.break()
}
)
println("执行循环体后的代码")

对Breaks进行省略:

1
2
3
4
5
6
7
8
9
import scala.util.control.Breaks._

breakable(
for (i <- 1 to 10) {
print(i + " ")
if (i == 5) break()
}
)
println("执行循环体后的代码")
  • 实现continue

可以利用前面提到的循环守卫的方式,也可以利用if判断的方式做到

参考文章

  1. 弱类型、强类型、动态类型、静态类型语言的区别是什么? - 知乎 (zhihu.com)

Scala学习笔记-入门(2)-Scala基础语法
http://example.com/2022/04/29/Scala学习笔记-入门-2-Scala基础语法/
作者
EverNorif
发布于
2022年4月29日
许可协议