Scala基础语法中的关键点

由于工作的需要,最近开始学习Scala和Flink,下面是学习途中做的笔记,略作了修改,希望能够给正在学习Scala的朋友一些帮助。

一、变量的定义

Scala变量使用两种方式定义:

  • var

    • 使用var定义的变量的值可以被修改。
    1
    var a: String = "MaphicalYng"  // 格式:var <变量名>: <变量类型> = <变量值>
  • val

    • 使用val定义的变量不能被修改。因为在日常对象使用中,我们很少去修改对象引用本身,而只是修改对象的属性。其次,val是线程安全的,Scala设计者推荐使用val
    1
    val a: String = "MaphicalYng"  // 格式:val <变量名>: <变量类型> = <变量值>

二、数据类型

所有的类型都是对象,没有基本数据类型。

例如:

1
1.toString  // 等同于 1.toString()

这里toString是一个方法,在Scala中,没有形参的方法可以不加括号进行调用。可以对1进行方法调用,说明1在Scala中是一个对象。

注意点:

  • 数据类型可以分为两大类,值类型和引用类型,分别是下面两个类的子类:
    • AnyVal:值类型的父类
    • AnyRef:引用类型的父类
  • 有一个所有类的父类:根类——Any

1.默认类型

整数类型默认为Int类型,浮点型默认为Double类型。Scala会自动将低精度转换为高精度,例如Float类型的值可以赋给Double类型的值,但不能相反。

2.浮点数精度

Float精度为点后6位,多位会丢失,一般使用Double因为精度高。

三、数据类型体系图

AnyVaI  Double  Long  Int  Short  Byte  unit  StringOps  (Scala collections)  AnyRef  (Other Scala classes)  (all java classes)  Null  Nothing  Subtype  Implicit Conversion  Class Hierarchy

四、字符类型

1.Char类型

Char类型为一个Unicode字符,可以使用正数表示,范围在U+0000 - U+FFFF。可以当作整数进行运算。

2.赋值逻辑

将一个表达式和一个常量赋值给一个变量时,逻辑不同:

  • 表达式:

    • 编译器在将表达式给变量赋值时,会先进行表达式的计算,此时会进行两个工作:(1)类型转换;(2)范围判断。所以当表达式的值为一个高精度类型时,就不能将此表达式的值赋值给一个低精度类型,典型例子:
    1
    var a: Char = 97 + 1 // 报错  
  • 常量:

    • 编译器将常量给变量赋值时,只会进行范围的判断,即常量值在变量类型允许的范围中即可,而不会进行类型的判断,此时可以将高精度的常量赋值给低精度类型的变量,典型例子:
    1
    var a: Char = 98 // 正常运行  

五、Unit类型、Null类型和Nothing类型

  • Unit类型相当于Java中的void,表示空值,此类型只有一个值:()

  • Null类型是所有AnyRef类型的子类,只有一个值:null。可以赋值给任意引用类型(AnyRef),但不能赋值给值类型(AnyVal)。

  • Nothing类型是所有类型的子类,可以赋值给所类型(Any),开发中可以将Nothing返回给任何函数调用或类型变量。

六、变量名称的格式

和Java的变量命名风格基本相同,其中不同的是:

  • 首字符可以是操作符(+-*/),但若首字母是操作符,则后续必须也是操作符,且至少跟1个,操作符不能出现在变量名的中间或最后。
  • 关键字也可以作为变量名,例如true,使用反引号(``)包括起来即可。

七、变量支持代码块赋值

在给变量赋值时,可以使用一个大括号将一些代码括住,这些代码的执行结果作为变量的值赋给变量,例:

1
var a1: Int = { 1 + 1 }

八、表达式的值

Scala中任意表达式都有返回值,表达式可以是一个代码块,使用大括号括住,具体的值取决于表达式代码块的最后一行内容。

九、for循环

Scala中的循环和Java中有较大差异,Scala中有3种for循环:前后范围闭合的循环、前闭合后开放的循环、对可遍历对象的循环。

(1)前后范围闭合的循环

1
2
3
for (i <- start to end) {
println(i)
}

此种方式会将start和end都包括进去,打印值为start到end。

(2)前闭合后开放的循环

1
2
3
for (i <- start until end) {
println(i)
}

此种方式会将start包括进去,而不会包括end,打印值为start到end-1。数据范围部分也可以使用Range(start, end)替换。

(3)对可遍历对象的循环

1
2
3
4
val list: List[Int] = List(1, 2, 3) 
for (i <- list) {
println(i)
}

此种方式会将list的值遍历打印出来。

1.循环守卫

循环守卫是一句在for循环中的判断语句,当此语句判断为true时,会进入循环体中执行,若判断为false,会跳过这次循环,进入下一次循环。

1
2
3
for (i <- start to end if i != 2) {
println(i)
}

此处打印的值为(start=1,end=3):1,3,跳过了2.

2.引入变量

可以在循环头部语句中加入变量,例:

1
2
3
for (i <- start to end; j = 5 - i) {
println(j)
}

次数会引入变量j,打印结果为(start=1,end=3):4,3,2.

3.嵌套循环

在循环头部写入嵌套循环,例:

1
2
3
4
for (i <- start to end; j <- i to end) {
println(i)
println(j)
}

相当于等价代码:

1
2
3
4
5
6
for (i <- start to end) {
for (j <- i to end) {
println(i)
println(j)
}
}

4.返回值

for循环可以有返回值,使用yield关键字可以将返回值存储到一个Vector集合中。一个常用应用方式是,将一堆数据经过业务逻辑处理之后形成新的数据集合,或者进行多次处理形成集合,示例代码:

1
2
3
4
5
6
7
8
val list1: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)
val res = for (i <- list1) yield {
if (i % 2 == 0) {
i * 2
} else {
i * 3
}
}

上述代码功能为:将list1列表中的所有偶数扩大2倍,奇数扩大3倍,并存储在新的集合中。

注:for的小括号可以换为大括号并换行写条件。

十、break和continue

Scala中没有breakcontinue关键字,而是使用其他方式替代其功能。

  1. break

使用函数util.control.Breaks.break以及高阶函数util.control.Breaks.breakable来实现break关键字的功能,示例代码:

1
2
3
4
5
6
7
breakable{
for (i <- Range(1, 20)) {
if (i == 6) {
break()
}
}
}

break()函数会抛出一个breakException,而高阶函数breakable函数会catch这个异常,实现跳出循环的效果。

  1. continue

使用循环守卫或者循环体内的if判断来实现continue的效果,示例代码:

1
2
3
for (i <- Range(1, 20) if i != 14) {
println(i)
}

上述代码表示当i不等于14时进入循环体,实现了当i等于14时执行continue的效果。

第二种方式是使用循环体内部的if判断,实例代码为:

1
2
3
4
5
for (i <- Range(1, 20)) {
if (i != 14) {
println(i)
}
}

十一、函数的定义

基本语法:

def 函数名([参数名: 参数类型, …])[[: 返回值类型] =] {
  语句…
  return 返回值
}

关于返回值:

  • : 返回值类型 =
    • 表示有返回值,且返回值类型确定
  • =
    • 表示返回值类型不确定,需要使用类型推导
  • (空)
    • 表示没有返回值,return无效

1.函数定义位置

函数定义位置不影响其作用域,在方法中定义方法,可以和其他方法同等使用。

2.函数参数的默认值

函数的参数可以有默认值,在调用函数时,可以不给实参,函数将使用默认值;若只想修改部分参数的值,可以使用带名参数,指定参数名称为参数赋值。代码示例:

1
2
3
4
5
6
def databaseConnect(ip: String = "127.0.0.1", port: Int = 3306,
user: String = "root", password: String = "root"): Unit = {
println(ip + port + user + password)
}
// 调用,当需要不按顺序修改参数的值时,可以指定参数名称赋值
databaseConnect("192.168.1.2", password = "admin123")

注:Scala函数的形参默认是val类型,因此不能在函数中修改。

3.可变参数

Scala函数支持可变参数,使用星号表示,例:

1
2
3
4
5
def func1(args: Int*): Unit = {
for (i <- args) {
print(i)
}
}

其中args可以接受多个参数,类型都为Intargs为一个序列,包含了多个参数的值。可以和普通参数一同使用,但使用时可变参数一定放在最后。调用方法:

1
func1(1, 2, 3, 4, 5, 6, 7, 8, 98)  

4.过程

返回值类型为Unit的函数称为过程。

十二、函数的递归

递归函数不能自动判断返回类型,必须指定返回的数据类型。

十三、惰性函数

一个有返回值的函数,当它被调用且返回值被赋值给一个被声明为lazyval类型时,这个函数将的执行将被推迟,直到这个val类型被使用时,函数才会被执行。在此之前,即使存在赋值语句,函数仍不会被执行。代码示例:

1
2
3
4
5
6
7
def t2(n: Int): Int = {
println("函数被执行")
n * 2
}
lazy val result: Int = t2(200)
println("赋值语句结束")
println(result)

这段代码的执行结果为:

赋值语句结束
函数被执行
400

result被赋值时,t2函数并不会立即执行,而是在使用result的值时才会被执行。

注:lazy关键字只能用于val类型的值,不能修饰var类型的变量。当lazy修饰一个变量时,该变量值的分配也会被推迟。

十四、异常的类型

Scala异常体系沿用了Java的异常体系,Java的各种异常类型被Scala使用。

十五、异常的处理

Scala中也使用trycatch来捕获和处理异常,但是用法和Java不尽相同,捕获和处理异常的代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
def mayException(): Int = {
5 / 0
}
try {
mayException()
} catch {
case e: ArithmeticException => println(e.getMessage)
case e: Exception => {
println("捕获了一个异常")
e.printStackTrace()
}
}

上述代码使用trycatch捕获并处理了一个算数异常,在catch块中,Scala使用case关键字来捕获异常,使用=>关键字来声明处理异常的代码,这里可以是单行代码也可以是代码块。可以添加finally块,此块中的代码无论异常捕获发生与否都会执行。

1.声明某个方法可能抛出异常

使用throw注解标注方法来声明这个方法可能抛出异常:

1
2
3
4
@throws(classOf[ArithmeticException])
def mayException(): Int = {
5 / 0
}

其中classOf[ArithmeticException]相当于Java中的ArithmeticException.class

十六、面向对象编程

1.Scala类的属性和Java的关系

Scala中的类的属性默认都是private,而在底层Java字节码中,生成了对应的publicgettersetter方法。

Scala中的类默认权限修饰符是public,不能显式添加public修饰符,且一个类文件中可以包含多个类,且全部可以为public

2.属性定义

Scala在类中定义属性时,需要显式初始化,而不能像Java中声明后不进行初始化,可以根据初始化的值进行自动类型判断。

如果初始化的值为null,则一定要加类型,否则属性的类型将被自动推断为Null

若需要为各个类型的变量赋值默认值,使用下划线“_”进行赋值,下划线为各个类型赋值的默认值见表:

Byte Short Int Long  Float Double  string  0.0  null  false

3.对象

Scala对象内存布局和Java相同,栈中的引用对象的变量保存指向堆中对象的内存地址。

使用new关键字进行对象的实例化,对于构造器没有形参的类,可以不加括号。

4.类

Scala中的类定义使用class关键字,构造器声明在class关键字类名后面,其成员变量使用var或者val在类的块中进行声明,声明为private的属性使用gettersetter进行外部访问,声明方法和Java不同,示例代码:

1
2
3
4
5
6
7
8
9
10
class TestClass(left: String, right: String) {
private var _pp: String = _
def pp = _pp
def pp_=(newValue: String): Unit = {
_pp = newValue
}
def greet(name: String): Unit = {
println(left + name + right)
}
}

注:

  • TestClass后面的参数列表为默认构造器(主构造器)的形参列表,当参数名称前不加varval时,其权限为private,添加varval时,其权限为public
  • 权限为private的属性,getter方法名为属性名称;setter方法名为属性名_=。在调用时,可以直接使用等于号对属性值进行赋值,而不用显式调用函数名称。例:
1
2
val t = new TestClass(left = "left", right = "right")
t.pp = "?????"
  • 写在类名后面的构造器称为主构造器,在类定义中的方法名为this的构造器为辅助构造器。
  • 主构造器在执行时会调用类定义中的所有语句,所以可以在定义类时进行属性的处理或者其他工作。
  • Scala通过构造器形参列表的不同来区分不同的构造器,辅助构造器在定义时必须显式调用主构造器(直接调用或调用其他调用了主构造器的构造器)。
  • 为了和Java的可互操作性,属性可以使用@BeanProperty注解标注,用于生成和Java兼容的getXxxsetXxx方法,与原有的Scala的getter和setter方法可以共存。

5.类的继承

Scala和Java相同,都是只能单继承,使用extends关键字集成基类。

6.object

Scala中不仅有class关键字来声明一个类,还有object关键字来声明对象类型。对象类型指的是这个对象类型定义所代表的类的单例对象。

Objects are single instances of their own definitions. You can think of them as singletons of their own classes.

在编译后的Java字节码中,表现为一个伴生类,我们在程序中使用的是伴生类的实例化对象。

Scala程序的入口声明在一个object中,方法名为main,参数列表为字符串数组,例:

1
2
3
4
object Exec {
def main(args: Array[String]): Unit = {
}
}

7.trait

trait中文名为特质,Scala中的trait相当于Java中的interface,用于在类之间共享接口和属性,但它不能被实例化。

十七、包机制

1.包的声明

Scala中的包比Java中更灵活,有三种声明包的方式:

第一种:

1
2
package cn.maphical.modules
class PackageImport {}

第二种:

1
2
3
package cn.maphical
package modules
class PackageImport{}

第三种:

1
2
3
4
5
package cn.maphical {
package modules {
class PackageImport {}
}
}

因此可以在同一个文件中声明不同的包,更加灵活。

子包可以使用父包中的内容,而不用显式引入。

就近原则

如果有两个相同名称的类,不使用包路径直接引用,则会优先使用本包中的类,由近及远。

2.包对象

一个包对象(package object)是对包功能的补充。现有一个包cn.maphical.modules,定义这个包的包对象,在其父包cn.maphical中定义package object modules,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.maphical
package object modules {
val name = "Scala"
def sayHi(): Unit = {
println("HI from package object")
}
}
package modules {
object Test {
def main(args: Array[String]): Unit = {
println("name=" + name)
sayHi()
}
}
}

在cn.maphical包中定义了modules的包对象后,其内部的变量和方法都可以在modules包中使用了。

包对象的底层机制

使用反编译工具对上述代码进行反编译,可以得出:包对象在字节码中的存在形式为名为packagepackage$的两个类,package$中存在一个静态对象,此对象的方法被Test类中的main方法调用,反编译后的代码如下:

文件名:package$.class

1
2
3
4
5
6
7
8
9
10
11
package cn.maphical.modules;
public final class package$ {
public static final package$ MODULE$ = new package$();
private static final String name = "Scala";
public String name() {
return name;
}
public void sayHi() {
scala.Predef$.MODULE$.println("HI from package object");
}
}

文件名:package.class

1
2
3
4
5
6
7
8
9
10
package cn.maphical.modules;
import scala.reflect.ScalaSignature;
public final class package {
public static void sayHi() {
package$.MODULE$.sayHi();
}
public static String name() {
return package$.MODULE$.name();
}
}

文件名:Test$.class

1
2
3
4
5
6
7
8
9
10
11
package cn.maphical.modules;
public final class Test$ {
public static final Test$ MODULE$ = new Test$();
public void main(String[] args) {
scala.Predef$.MODULE$.println((newStringBuilder(5))
.append("name=")
.append(package$.MODULE$.name())
.toString());
package$.MODULE$.sayHi();
}
}

Test$中的main方法调用了package$中静态对象的各种方法,这些方法都是在Scala包对象中定义的。

包对象中可以定义任何内容,而不仅仅是变量和方法。 例如,包对象经常用于保存包级作用域的类型别名和隐式转换。 包对象甚至可以继承 Scala 的类和特质。

按照惯例,包对象的代码通常放在名为 package.scala的源文件中。

3.伴生类和伴生对象

在一个文件中出现了同名的类(class)和对象(object)时,class称为伴生类,object称为伴生对象。由于Scala中没有static关键字,为了和Java兼容(Java有static关键字),需要将静态内容写在伴生对象中,非静态内容写在伴生类中。

1
2
3
4
5
6
7
8
class ClassAndObject {
private val text = "Scala"
}
object ClassAndObject {
def test(c: ClassAndObject): Unit = {
println(c.text)
}
}

伴生对象可以访问伴生类中的私有方法和属性。

4.包访问权限

Scala中的包访问权限与Java稍有不同,Scala中没有public关键字,其默认的方法和属性权限为public

  • private:私有权限,只在类的内部和伴生对象中可以使用。
  • protected:受保护权限,只能子类访问,与Java不同,同包不能访问。

访问权限的扩大:

protectedprivate关键字可以手动扩大权限,指定能够访问此内容的包:

1
private[modules] val text = "Scala"

上述代码表示,text属性在private基础上,对modules包以及它的子包有可访问权限。

5.包的引入

与Java不同的点为:

  • import语句可以不出现在文件头部,可以在需要时再引入,但其作用域为其被包含的大括号。
  • 引入所有包,使用下划线(_)
  • 使用选择器导入部分包部分类,例:import scala.collection.mutable.{HashMap, HashSet},即导入了HashMapHashSet这两个类
  • 将导入的类重命名,例:import java.util.{HashMap => Map},将HashMap重命名为Map

十八、柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel Gottlob Frege 发明的。

——百度百科

Scala中的函数柯里化使用多个参数列表实现,一个函数可以有多个参数列表,而这些参数列表不需要必须同时提供。在只提供一个参数列表的实参时,函数返回带有这些参数的值的新函数,此新函数可以接受剩余的参数列表,此函数可以作为一个普通函数使用,并返回正确的值。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object CurryingTest {
def curringTest(seq: List[Int])(func: (Int, Int) => Int): Int = {
var result = seq.head
for (item <- seq; if item != seq.head) {
result = func(result, item)
}
result
}
def main(args: Array[String]): Unit = {
val cat = new Cat
val list: List[Int] = List[Int](1, 2, 3, 4, 5, 6, 7)
val result = curringTest(list) _ // 这里使用下划线代替没有提供实参的参数列表
println(result(_ * _))
}
}

上述代码中的result既是curryingTest函数在接受了一个List后形成的函数,此函数可以作为一个新的函数进行调用,并接受原有函数的未提供参数列表作为其参数列表。

十九、内部类

Scala的内部类和Java不同之处在于,Java中在某类的内部定义的类,是此类的成员变量,和外部类绑定。而Scala中在某类内部定义的类,和此类的对象绑定,即每个此类实例化产生的每个对象都有一个此内部类的定义,不同对象的此内部类是“不同的类型”。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object InnerClass {
def main(args: Array[String]): Unit = {
val a = new InnerClassTest
val b = new InnerClassTest
val innerA = a.newInnerA
val innerB = b.newInnerA
a.setInnerInst(innerA) // 正常赋值
a.setInnerInst(innerB) // 非法赋值
}
}
class InnerClassTest {
class InnerClassA {}
var innerClassInst: InnerClassA = _
def newInnerA = new InnerClassA
def setInnerInst(inst: InnerClassA): Unit = this.innerClassInst = inst
}

上述中将innerB对象赋值给对象a的成员变量时出错,原因是innerAinnerB不是同一个类型,它们的类型分别是:a.InnerClassAb.InnerClassA(和对象绑定)。


Scala基础语法中的关键点
https://maphical.cn/2020/09/scala-basic-key-points/
作者
MaphicalYng
发布于
2020年9月2日
许可协议