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 |
|
这里toString
是一个方法,在Scala中,没有形参的方法可以不加括号进行调用。可以对1进行方法调用,说明1在Scala中是一个对象。
注意点:
- 数据类型可以分为两大类,值类型和引用类型,分别是下面两个类的子类:
AnyVal
:值类型的父类AnyRef
:引用类型的父类
- 有一个所有类的父类:根类——
Any
。
1.默认类型
整数类型默认为Int
类型,浮点型默认为Double
类型。Scala会自动将低精度转换为高精度,例如Float
类型的值可以赋给Double
类型的值,但不能相反。
2.浮点数精度
Float
精度为点后6位,多位会丢失,一般使用Double
因为精度高。
三、数据类型体系图
四、字符类型
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 |
|
八、表达式的值
Scala中任意表达式都有返回值,表达式可以是一个代码块,使用大括号括住,具体的值取决于表达式代码块的最后一行内容。
九、for循环
Scala中的循环和Java中有较大差异,Scala中有3种for循环:前后范围闭合的循环、前闭合后开放的循环、对可遍历对象的循环。
(1)前后范围闭合的循环
1 |
|
此种方式会将start和end都包括进去,打印值为start到end。
(2)前闭合后开放的循环
1 |
|
此种方式会将start包括进去,而不会包括end,打印值为start到end-1。数据范围部分也可以使用Range(start, end)
替换。
(3)对可遍历对象的循环
1 |
|
此种方式会将list的值遍历打印出来。
1.循环守卫
循环守卫是一句在for循环中的判断语句,当此语句判断为true
时,会进入循环体中执行,若判断为false
,会跳过这次循环,进入下一次循环。
1 |
|
此处打印的值为(start=1,end=3):1,3,跳过了2.
2.引入变量
可以在循环头部语句中加入变量,例:
1 |
|
次数会引入变量j,打印结果为(start=1,end=3):4,3,2.
3.嵌套循环
在循环头部写入嵌套循环,例:
1 |
|
相当于等价代码:
1 |
|
4.返回值
for循环可以有返回值,使用yield
关键字可以将返回值存储到一个Vector
集合中。一个常用应用方式是,将一堆数据经过业务逻辑处理之后形成新的数据集合,或者进行多次处理形成集合,示例代码:
1 |
|
上述代码功能为:将list1
列表中的所有偶数扩大2倍,奇数扩大3倍,并存储在新的集合中。
注:for的小括号可以换为大括号并换行写条件。
十、break和continue
Scala中没有break
和continue
关键字,而是使用其他方式替代其功能。
- break
使用函数util.control.Breaks.break
以及高阶函数util.control.Breaks.breakable
来实现break
关键字的功能,示例代码:
1 |
|
break()
函数会抛出一个breakException
,而高阶函数breakable
函数会catch这个异常,实现跳出循环的效果。
- continue
使用循环守卫或者循环体内的if判断来实现continue
的效果,示例代码:
1 |
|
上述代码表示当i
不等于14时进入循环体,实现了当i
等于14时执行continue
的效果。
第二种方式是使用循环体内部的if判断,实例代码为:
1 |
|
十一、函数的定义
基本语法:
def 函数名([参数名: 参数类型, …])[[: 返回值类型] =] { 语句… return 返回值 }
关于返回值:
- : 返回值类型 =
- 表示有返回值,且返回值类型确定
- =
- 表示返回值类型不确定,需要使用类型推导
- (空)
- 表示没有返回值,
return
无效
- 表示没有返回值,
1.函数定义位置
函数定义位置不影响其作用域,在方法中定义方法,可以和其他方法同等使用。
2.函数参数的默认值
函数的参数可以有默认值,在调用函数时,可以不给实参,函数将使用默认值;若只想修改部分参数的值,可以使用带名参数,指定参数名称为参数赋值。代码示例:
1 |
|
注:Scala函数的形参默认是val
类型,因此不能在函数中修改。
3.可变参数
Scala函数支持可变参数,使用星号表示,例:
1 |
|
其中args
可以接受多个参数,类型都为Int
,args
为一个序列,包含了多个参数的值。可以和普通参数一同使用,但使用时可变参数一定放在最后。调用方法:
1 |
|
4.过程
返回值类型为Unit
的函数称为过程。
十二、函数的递归
递归函数不能自动判断返回类型,必须指定返回的数据类型。
十三、惰性函数
一个有返回值的函数,当它被调用且返回值被赋值给一个被声明为lazy
的val
类型时,这个函数将的执行将被推迟,直到这个val
类型被使用时,函数才会被执行。在此之前,即使存在赋值语句,函数仍不会被执行。代码示例:
1 |
|
这段代码的执行结果为:
赋值语句结束 函数被执行 400
即result
被赋值时,t2
函数并不会立即执行,而是在使用result
的值时才会被执行。
注:lazy
关键字只能用于val
类型的值,不能修饰var
类型的变量。当lazy
修饰一个变量时,该变量值的分配也会被推迟。
十四、异常的类型
Scala异常体系沿用了Java的异常体系,Java的各种异常类型被Scala使用。
十五、异常的处理
Scala中也使用try
和catch
来捕获和处理异常,但是用法和Java不尽相同,捕获和处理异常的代码示例如下:
1 |
|
上述代码使用try
和catch
捕获并处理了一个算数异常,在catch
块中,Scala使用case
关键字来捕获异常,使用=>
关键字来声明处理异常的代码,这里可以是单行代码也可以是代码块。可以添加finally
块,此块中的代码无论异常捕获发生与否都会执行。
1.声明某个方法可能抛出异常
使用throw注解标注方法来声明这个方法可能抛出异常:
1 |
|
其中classOf[ArithmeticException]
相当于Java中的ArithmeticException.class
。
十六、面向对象编程
1.Scala类的属性和Java的关系
Scala中的类的属性默认都是private
,而在底层Java字节码中,生成了对应的public
的getter
和setter
方法。
Scala中的类默认权限修饰符是public
,不能显式添加public
修饰符,且一个类文件中可以包含多个类,且全部可以为public
。
2.属性定义
Scala在类中定义属性时,需要显式初始化,而不能像Java中声明后不进行初始化,可以根据初始化的值进行自动类型判断。
如果初始化的值为null
,则一定要加类型,否则属性的类型将被自动推断为Null
。
若需要为各个类型的变量赋值默认值,使用下划线“_”进行赋值,下划线为各个类型赋值的默认值见表:
3.对象
Scala对象内存布局和Java相同,栈中的引用对象的变量保存指向堆中对象的内存地址。
使用new
关键字进行对象的实例化,对于构造器没有形参的类,可以不加括号。
4.类
Scala中的类定义使用class
关键字,构造器声明在class
关键字类名后面,其成员变量使用var
或者val
在类的块中进行声明,声明为private
的属性使用getter
和setter
进行外部访问,声明方法和Java不同,示例代码:
1 |
|
注:
- 在
TestClass
后面的参数列表为默认构造器(主构造器)的形参列表,当参数名称前不加var
或val
时,其权限为private
,添加var
或val
时,其权限为public
。 - 权限为
private
的属性,getter方法名为属性名称;setter方法名为属性名_=
。在调用时,可以直接使用等于号对属性值进行赋值,而不用显式调用函数名称。例:
1 |
|
- 写在类名后面的构造器称为主构造器,在类定义中的方法名为
this
的构造器为辅助构造器。 - 主构造器在执行时会调用类定义中的所有语句,所以可以在定义类时进行属性的处理或者其他工作。
- Scala通过构造器形参列表的不同来区分不同的构造器,辅助构造器在定义时必须显式调用主构造器(直接调用或调用其他调用了主构造器的构造器)。
- 为了和Java的可互操作性,属性可以使用
@BeanProperty
注解标注,用于生成和Java兼容的getXxx
和setXxx
方法,与原有的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 |
|
7.trait
trait中文名为特质,Scala中的trait
相当于Java中的interface
,用于在类之间共享接口和属性,但它不能被实例化。
十七、包机制
1.包的声明
Scala中的包比Java中更灵活,有三种声明包的方式:
第一种:
1 |
|
第二种:
1 |
|
第三种:
1 |
|
因此可以在同一个文件中声明不同的包,更加灵活。
子包可以使用父包中的内容,而不用显式引入。
就近原则
如果有两个相同名称的类,不使用包路径直接引用,则会优先使用本包中的类,由近及远。
2.包对象
一个包对象(package object)是对包功能的补充。现有一个包cn.maphical.modules
,定义这个包的包对象,在其父包cn.maphical
中定义package object modules
,具体代码如下:
1 |
|
在cn.maphical包中定义了modules
的包对象后,其内部的变量和方法都可以在modules
包中使用了。
包对象的底层机制
使用反编译工具对上述代码进行反编译,可以得出:包对象在字节码中的存在形式为名为package
和package$
的两个类,package$
中存在一个静态对象,此对象的方法被Test
类中的main
方法调用,反编译后的代码如下:
文件名:package$.class
1 |
|
文件名:package.class
1 |
|
文件名:Test$.class
1 |
|
Test$
中的main
方法调用了package$
中静态对象的各种方法,这些方法都是在Scala包对象中定义的。
包对象中可以定义任何内容,而不仅仅是变量和方法。 例如,包对象经常用于保存包级作用域的类型别名和隐式转换。 包对象甚至可以继承 Scala 的类和特质。
按照惯例,包对象的代码通常放在名为 package.scala
的源文件中。
3.伴生类和伴生对象
在一个文件中出现了同名的类(class)和对象(object)时,class称为伴生类,object称为伴生对象。由于Scala中没有static
关键字,为了和Java兼容(Java有static
关键字),需要将静态内容写在伴生对象中,非静态内容写在伴生类中。
1 |
|
伴生对象可以访问伴生类中的私有方法和属性。
4.包访问权限
Scala中的包访问权限与Java稍有不同,Scala中没有public
关键字,其默认的方法和属性权限为public
。
- private:私有权限,只在类的内部和伴生对象中可以使用。
- protected:受保护权限,只能子类访问,与Java不同,同包不能访问。
访问权限的扩大:
protected
和private
关键字可以手动扩大权限,指定能够访问此内容的包:
1 |
|
上述代码表示,text
属性在private
基础上,对modules
包以及它的子包有可访问权限。
5.包的引入
与Java不同的点为:
import
语句可以不出现在文件头部,可以在需要时再引入,但其作用域为其被包含的大括号。- 引入所有包,使用下划线(_)
- 使用选择器导入部分包部分类,例:
import scala.collection.mutable.{HashMap, HashSet}
,即导入了HashMap
和HashSet
这两个类 - 将导入的类重命名,例:
import java.util.{HashMap => Map}
,将HashMap
重命名为Map
十八、柯里化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
——百度百科
Scala中的函数柯里化使用多个参数列表实现,一个函数可以有多个参数列表,而这些参数列表不需要必须同时提供。在只提供一个参数列表的实参时,函数返回带有这些参数的值的新函数,此新函数可以接受剩余的参数列表,此函数可以作为一个普通函数使用,并返回正确的值。
代码:
1 |
|
上述代码中的result
既是curryingTest
函数在接受了一个List
后形成的函数,此函数可以作为一个新的函数进行调用,并接受原有函数的未提供参数列表作为其参数列表。
十九、内部类
Scala的内部类和Java不同之处在于,Java中在某类的内部定义的类,是此类的成员变量,和外部类绑定。而Scala中在某类内部定义的类,和此类的对象绑定,即每个此类实例化产生的每个对象都有一个此内部类的定义,不同对象的此内部类是“不同的类型”。
例:
1 |
|
上述中将innerB
对象赋值给对象a
的成员变量时出错,原因是innerA
和innerB
不是同一个类型,它们的类型分别是:a.InnerClassA
和b.InnerClassA
(和对象绑定)。