Scala 学习笔记
1.函数式编程
函数式编程:函数式编程把函数当作一等公民,充分利用函数,支持函数的多种使用方式。在scala中,函数可以像变量一样,既可以作为函数的参数使用,也可以将函数赋值给下一个变量。函数的创建不用依赖与类或者变量。
1.1 函数的注意事项
- 函数的形参列表可以有多个,如果函数没有参数,调用时可以省略 '()';
- 形参列表和返回值列表的数据类型可以是值类型和引用类型;
- 函数可以根据函数体最后一行自行推断返回值的类型,可以省略return关键字,此时返回值类型也可以省略;
- 如果函数明确使用return关键字,那么函数返回就不能使用自行推断,此时需要明确写出返回值类型,如果不写,return就不会生效,返回值为 ‘()’;
- 如果函数明确声明无返回值(声明Unit),那么函数体中即使使用return关键字也不会有返回值;
- 如果明确函数无返回值或者不确定返回值类型,那么返回值类型可以省略或者声明为‘Any’;
- 递归不能使用类型推断;
1.2惰性函数
当函数的返回值被声明为lazy时,函数的执行将被推迟,知道我们首次对此取值,该函数才会执行。惰性几何在需要时提供其元素,无需预先计算,带来的好处是我们可以将耗时的任务推迟到绝对需要的时候。
注意事项:
- lazy不能修饰var类型的变量。
- 在调用函数时加了‘lazy’会导致函数的执行会被推迟,我们在声明变量时,如果也加了‘lazy’,那么变量值的分配也会被推迟。
2.面相对象编程
将数据及其操作放到一个容器当中(class)
2.1 类与对象的区别与联系
- 类是抽象的,概念的,代表一类事物;
- 对象是实际的,代表一个具体的实例;
- 类是对象的模版,对象是类的一个个体,对应一个实例。
2.2 scala中如何定义类
[修饰符] class 类名{
类体
}
2.3定义类的注意事项:
- scala语法中,类不用声明为‘public’,所有这些类都具有共有可见性(默认就是public)
- 一个scala源文件可以有多个类,而且默认都是public
2.4 属性/成员变量
2.5 构造器
构造器是类的一种特殊方法,它的主要作用是完成对新对象的初始化
scala的构造器包含 :主构造器 和 辅助构造器
基本语法:
class 类名(形参列表) { //主构造器
// 类体
def this(形参列表){ //辅助构造器1
}
def this(形参列表){ // 辅助构造器2
}
//辅助构造器可以有多个,编译器通过不同参数来区分
}
构造器参数
- Scala类的主构造器的形参未用任何修饰符修饰,那么这个参数是局部变量;
- 如果参数使用val关键字声明,那么Scala会将参数作为类的私有的只读属性使用;
- 如果参数使用var关键字声明,那么那么Scala会将参数作为类的成员属性使用,并会提供属性对应的xxx()[类似getter]/xxx_$eq()[类似setter]方法,即这时的成员属性是私有的,但是可读写。
对象创建的流程分析
1.加载类的信息,包括属性信息和方法信息
2.在内存中(堆)开辟空间
3.使用父类的构造器初始化
4.使用主构造器初始化
5.使用辅助构造器初始化
6.将开辟的对象地址赋值给变量名这个引用
2.6 包
scala包的作用
- 区分相同名字的类
- 可以很好的和管理类
- 控制访问范围
- 可以对类的功能进行扩展
** scala包的使用细节以及注意事项**
1.作用域原则:可以直接向上访问。即:scala中子包可以直接访问父包中的内容,在子包和父包类重名时,默认采用就近原则,如果希望指定使用某个类,则带上包名即可。
2.父包要访问子包的内容时,需要import对应的类等
3.包对象:包可以包含类、对象、特质,但是不能包含函数、方法。scala提供了包对象的概念来解决这一问题
- 每一个包都可以有一个包对象
- 包对象名称需要和包名一致,一般用于对包的功能补充。
包的可见性
java中包的可见性
访问级别 | 访问控制修饰符 | 同类 | 同包 | 子类 | 不同包 |
---|---|---|---|---|---|
公开 | public | ✔ | ✔ | ✔ | ✔ |
受保护 | protected | ✔ | ✔ | ✔ | ✖ |
默认 | 没有修饰符 | ✔ | ✔ | ✖ | ✖ |
私有 | private | ✔ | ✖ | ✖ | ✖ |
scala中包的可见性和访问修饰符的使用
- 当属性访问为默认时,从底层看属性是private的,但是因为提供了 xxx_$eq()/xxx() 方法 类似于 seter getter 方法 ,因此从使用效果看是任何地方都可以访问的;
- 当方法访问权限为默认时,默认为public 访问权限;
- private为私有权限,只有类的内部和伴生对象中可用;
protected为受保护权限,scala中受保护权限比java中更严格,只能子类访问,同包无法访问; - scala中没有public 关键字;
- 包访问权限:
用例:
// 增加包访问权限后,private权限同时起作用,不仅同类可以使用,同时在package包下的其他类也可以使用
private [package] val p = ""
protected [package] val p2 =""
2.7 面向对象编程的三大特征
抽象
抽象:在定义一个类的时候,实际上就是把一类事物的共有的属性和行为提取出来,形成一个屋里模型,这种研究问题的方法称为抽象
封装
封装的本质就是把抽象出来的数据和对数据的操作封装到一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(成员方法),才能对数据进行操作。
** 封装的好处**
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理
继承
继承可以解决代码复用,当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有子类无需定义这些属性和方法,只需要通过继承父类即可。在scala的继承中,子类继承了父类的所有属性,只是私有属性不能直接访问,需要通过公共的方法访问。
** 继承的好处**
- 提高了代码的复用性
- 提高了代买的扩展性和维护性
2.8 抽象类
在scala中,通过abstract关键字标记不能被实例化的类。方法不用标记abstract,只需省略方法体即可,抽象类可以拥有抽象字段,抽象字段就是没有初始值的字段
抽象类的价值更多在于设计,是设计者设计好之后,让子类继承并实现抽象类
抽象类的使用细节
- 抽象类不能被实例化;
- 抽象类不一定要包含abstract方法;
- 一旦类包含了抽象方法或抽象属性,则这个类必须声明为abstract;
- 抽象方法不能有主体,不能使用abstract修饰;
- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法和抽象属性,除非它自己也声明为abstract;
- 抽象方法和抽象属性不能使用 ‘private’、‘final’来修饰,因为这些关键字都是和重写/实现相违背;
- 抽象类中可以有实现的方法;
- 子类重写抽象方法可以不用overwrite修饰。
2.9 静态属性和方法
伴生对象
- scala中伴生对象采用‘object’关键字声明,伴生对象中声明的全是“静态内容”,可以通过伴生对象名称直接调用。
- 伴生对象对应的类称为伴生类,伴生对象的名称必须和伴生类名一致
- 伴生对象中的属性和方法可以通过伴生对象名直接调用访问;
- 伴生对象昂其实就是类的静态方法和成员的集合;
- 从底层原理看,伴生对象实现静态特性是依赖于 public static final MODULE$实现的;
- 伴生对象的声明应该和伴生类的声明放在同一个源码文件中;
2.10 特质(trait)等价于Java中的 interface + abstract class
在Scala中所有Java的接口都可以作为trait
trait中可以有抽象方法,也可以有非抽象方法;
动态混入
动态混入,可以在不修改类声明/定义的情况下扩展类的功能,具有高灵活性、低耦合的特点
用例
trait opt { // 特质
def insert(id:Int)={ // 方法(实现)
println("insert:" + id)
}
}
class Db1{
}
abstruct Db2{
}
abstruct Db3 {
def say()
}
object MixInDemo{
def main(args:Array[String])={
// 在不修改类的定义基础上,让他们可以使用trait 方法
val db1 = new Db1 with opt
}
// 在object没有方法,则可以直接实例化
val db2 = new Dd2 with opt
// 如果一个抽象类中有抽象方法,需要实现方法
val db3 = new Db3 with opt{
overwrite def say()={
println("hello world")
}
}
}
scala中创建对象的几种方式:
- new Class
- apply 创建
- 匿名子类方式
- 动态混入
叠加特质
构建对象时如果同时混入多个特质,则称之为叠加特质,那么特质的声明顺序从左到右,方法执行的顺序从右往左
scala中的特质如果调用super,并不是表示调用父特质的方法,而是向前(左边)继续查找特质,如果找不到才会去父特质查找
若果想要调用具体特质的方法,可以指定:super[trait].xxx(...),其中的泛型必须是特质的直接超类类型
在特质中重写抽象方法特例
富接口: 即特质中既有抽象方法,又有非抽象方法
2.11 嵌套类
// 外部类
class ScalaOuterClass {
class ScalaInnerClass { // 成员内部类
}
}
object ScalaOuterClass { //伴生对象
class ScalaStaticInnerClass{ // 静态内部类
}
}
内部类如果想要访问外部类的属性的两种方式
- 1.可以通过外部类对象访问:外部类名.this.属性名
- 2.外部类别名.属性名
类型投影
类型投影的作用是屏蔽外部对象对内部类的影响
投影类型的格式
外部类#内部类
在flink中就有大量使用类型投影的例子
import org.apache.flink.streaming.api.functions.co.BroadcastProcessFunction
import org.apache.flink.util.Collector
class broadFun extends BroadcastProcessFunction[String, String, String] with Serializable {
override def processBroadcastElement(value: String,
ctx: BroadcastProcessFunction[String, String, String]#Context,
out: Collector[String]) = ???
override def processElement(value: String,
ctx: BroadcastProcessFunction[String, String, String]#ReadOnlyContext,
out: Collector[String]) = ???
}
3. 隐式转换和隐式值
隐式转换函数是以implicit关键字声明的带有单个参数的参数的函数,这种函数将会自动应用,将值从一种类型转换成另一种类型。
// 编写一个隐式函数将Double转换成Int
implicit def doubleToInt(d:Double):Int={ // 底层生成 doubleToInt$1
d.toInt
}
val num:Int = 3.5 // 底层编译 int num = doubleToInt$1(3.5)
3.1 隐式转换的注意事项
- 隐式转换函数的函数名可以是任意的,隐式转换与函数名无关,至于函数签名(函数参数类型和返回值类型)有关
- 隐式函数可以有多个(即:隐式函数列表),但是需要保证在当前环境下,只有一个隐式函数被识别
3.2 隐式转换丰富类库功能
在实际开发中,如果想要增加新的功能就会需要改变源代码,这是很难接受,而且违背了软件开发的OCP开发原则(open close pricele),这种情况下,可以通过隐式转换函数给类动态的添加新功能。
// 隐式转换类:用于增加数值的区间判断
class NumBetween(i: Double) {
val bt = (from: Double, to: Double) => i >= from && i < to
}
object impliTest {
def main(args:Array[String]):Unit={
implicit def dbt(i: Double): NumBetween = new NumBetween(i)
val a = 10.0
a.bt(0,100) // 调用隐式方法
}
}
3.3 隐式值
隐式值也叫隐式变量,将某个形参变量标记为implicit,所以编译器会在方法省略隐式参数的情况下搜索作用域内的隐式值作为缺省参数。
- implicit的级别比默认值高;
- 隐式值匹配不能有二异性;
- 当一个隐式参数匹配不到隐式值,仍会使用默认值;
- 当设置了隐式参数,但没有隐式值,且没有默认值时,会报错
implicit val name:String = "jack" // 设置隐式值
def hello(implicit name:String)={
println(name + " hello")
}
hello //调用hello方法 会返回 jack hello,底层实现 是 hello$1(name);
3.4 隐式类
隐式类的特点
- 其所带的构造参数有且只有一个
- 隐式类必须定义在“类”,或者“伴生对象”,或者“包对象”里
- 隐式类不能是Cass class
- 作用域内不能有相同名称的标识符
隐式的转换时机
- 当方法中的参数类型以目标类型不一致时,或者是赋值时;
- 当第一项调用所在类中不存在的方法或成员时,编译器会自动将对象进行隐式转换(根据类型)
隐式转换的前提
- 不能存在二义性
- 不能嵌套使用