Scala 优雅入门 (五) 面向对象
语法
Scala 语法中,类并不声明为 public,所有这些类都具有公有可见性(即默认就是public)
一个 Scala 源文件可以包含多个类, 每个类默认都是 public
Scala 中声明一个属性,必须显式的初始化,然后根据初始化数据的类型自动推断,属性类型可以省略
如果赋值为 null, 则一定要加类型,因为不加类型, 那么该属性的类型就是 Null 类型.
如果在定义属性时,暂时不赋值,也可以使用符号_(下划线),让系统分配默认值, 这时要求属性必须给定数据类型
创建对象 val | var 对象名 [:类型] = new 类型 xxx
如果我们不希望改变对象的引用(即:内存地址), 应该声明为 val 性质的,否则声明为 var, Scala 设计者推荐使用 val ,因为一般来说,在程序中,我们只是改变对象属性的值,而不是改变对象的引用
当类型和后面 new 对象类型有继承关系即多态时,必须写类型
递归方法的返回值不能进行类型推断, 必须写类型
构造器
Scala 中构造器支持重载
Scala 类的构造器包括: 主构造器(只能有一个)
辅助构造器(可以有多个), 编译器通过不同参数(个数或类型)来区分
只有主构造器可以调用父类的构造器。辅助构造器不能直接调用父类的构造器
[code]class 类名(形参列表) { //括号内主构造器 // 类体 def this(形参列表) { // 辅助构造器 } def this(形参列表) { //辅助构造器可以有多个... } } //示例1 class Person { // 会生成一个默认的无参构造器 def this(name: String) { //所有的辅助构造器都会先调用主构造器 //为了底层能够实现对父类的初始化 this } def this(name: String, age: Int) { this } } //示例2 class Person(pName: String, pAge: Int) { //带参主构造器 var name = pName var age = pAge def show(): Unit = { println("name=" + this.name + " age=" + this.age) } } //调用 object ConstractorDemo { def main(args: Array[String]): Unit = { val person = new Person("jack", 18) //传入参数 person.show() } }
- Scala 构造器作用是完成对新对象的初始化,构造器没有返回值
- 主构造器的声明直接放置于类名之后
- 如果主构造器无参数,小括号可省略,构建对象时调用的构造方法的小括号也可以省略 val dog = new Dog
- 多个辅助构造器通过不同参数列表进行区分, 在底层就是构造器重载 辅助构造器无论是直接或间接,最终都一定要调用主构造器,执行主构造器的逻辑, 而且调用主构造器语句一定要放在辅助构造器的第一行
- 如果想让主构造器变成私有的,可以在 () 之前加上 private,这样用户不能直接通过主构造器来构造对象了 class C private() { }
- 辅助构造器的声明不能和主构造器的声明一致
构造器参数
Scala 类的主构造器的形参未用任何修饰符修饰,那么这个参数是局部变量。不可读\写 class Monster(mName:String,mAge:Int)
如果参数使用 val 关键字声明,那么Scala会将参数作为类的私有的只读属性使用 class Cat(val cName:String, val cAge:Int)
如果参数使用 var 关键字声明,那么那么Scala会将参数作为类的成员属性使用,并会提供属性对应的 xxx() [类似getter]/xxx_$eq()[类似setter] 方法,即这时的成员属性是私有的,但是可读写。 class Sheep(var sName:String)
@BeanProperty
将 Scala 字段加 @BeanProperty 时,这样会自动生成规范的 setXxx/getXxx 方法。这时可以使用 对象.setXxx() 和 对象.getXxx() 来调用属性, 许多 Java 工具(框架)都依赖这个命名习惯, 为了互操作性。也可以在构造器中使用
[code]class Car{ //默认生成 public name() 和 public name_$eq //如果加入了@BeanProperty 注解,就会再生成 public getName() 和 public setName() @BeanProperty var name:String = "宝马700" }
包
Scala会自动引入的常用包
- java.lang.*
- scala包
- Predef包
在scala中一个文件可以同时创建多个包
包也可以像嵌套类那样嵌套使用(包中有包)
作用域原则:可以直接向上访问。即: Scala 中子包中直接访问父包中的内容, 大括号体现作用域
在子包和父包 类重名时,默认采用就近原则,如果希望指定使用某个类,则带上包名即可。
父包要访问子包的内容时,需要 import 对应的类等
[code]object Test{ def main(args: Array[String]): Unit = { //使用com.rayfun.scala.Person import com.rayfun.scala.Person val person = new Person } }
在一般情况下:我们使用相对路径来引入包,只有当包名冲突时,使用绝对路径来处理
包对象
包可以包含类、对象和特质 trait,但不能包含函数/方法或变量的定义
在底层包对象(package object scala) 会生成两个类 class package / public final class package$
[code]package object scala { var name = "jack" //变量 def sayHi(): Unit = { //方法 println("package object scala sayHI()") } } package scala { // 整个包中的类/对象/子包都可以调用包对象中的属性和方法 … }
每个包都可以有一个包对象。需要在父包中定义它
包对象名称需要和包名一致,一般用来对包的功能补充
包可见性
java提供四种访问控制修饰符号控制方法和变量的访问权限(范围):
- 公开级别:用public 修饰,对外公开
- 受保护级别:用protected修饰,对子类和同一个包中的类公开
- 默认级别:没有修饰符号,向同一个包的类公开.
- 私有级别:用private修饰,只有类本身可以访问,不对外公开.
scala 设计者将访问的方式分成三大类: (1) 处处可以访问public (2) 子类和伴生对象能访问protected (3) 本类和伴生对象访问 private
包访问权限 (表示属性有了限制。同时包也有了限制), 这点和 Java 不一样,体现出 Scala 包使用的灵活性。 当然,也可以将可见度延展到上层包 private[rayfun] val description="zhangsan" 说明:protected 也可以变化,比如 protected[rayfun], 非常的灵活.
包的引入
Scala中,import 语句可以出现在任何地方, 同时他的作用范围就是 {} 块中
在需要时在引入包,缩小 import 包的作用范围,提高效率, 如果使用到 3 次及以上,则可以放在文件前面,否则可以使用就近引入
如果想要导入包中所有的类,Java 通过通配符 *,Scala 中采用下 _
如果不想要某个包中全部的类,而是其中的几个类,可以采用选取器,使用 {} 括起来即可
import scala.collection.mutable.{HashMap,HashSet}
如果引入的多个包中含有相同的类,那么可以将类进行重命名
import java.util.{ HashMap=>JavaHashMap, List}
如果某个冲突的类根本就不会用到,那么这个类可以直接隐藏掉
[code]import java.util.{ HashMap=>_, _} // 含义为 引入 java.util 包的所有类,但是忽略 HahsMap 类
继承
修改父类时,对应的子类就会继承相应的方法和属性的修改
子类继承了所有的属性(通过方法),但私有的属性不能直接访问,需要通过公共的方法访问
重写一个非抽象方法必须显式的声明,调用超类的方法使用super关键字
要测试某个对象是否属于某个给定的类,可以用 isInstanceOf 方法。用 asInstanceOf 方法将引用转换为子类的引用。classOf 获取对象的类名 s.getClass.getName(getSampleName)
覆写字段
Java 中只有方法的重写,没有属性/字段重写,
Scala 中有,子类改写父类的字段,我们称为覆写/重写字段。覆写字段需使用 override 修饰
- def 只能重写另一个 def (即:方法只能重写另一个方法)
- val 只能重写另一个 val 属性 或 重写不带参数的 def
- var 只能重写另一个抽象的 var 属性
抽象属性:声明未初始化的变量就是抽象的属性,抽象属性在抽象类
- 一个属性没有初始化,那么这个属性就是抽象属性
- 抽象属性在编译成字节码文件时,属性并不会声明,但是会自动生成抽象方法,所以类必须声明为抽象类
- 如果是覆写一个父类的抽象属性,那么 override 关键字可省略 [原因:父类的抽象属性,生成的是抽象方法,因此就不涉及到方法重写的概念,因此 override 可省略]
抽象类
在 Scala 中,通过 abstract关 键字标记不能被实例化的类。方法不用标记 abstract,只要省掉方法体即可。抽象类可以拥有抽象字段,抽象字段/属性就是没有初始值的字段
[code]//抽象类 abstract class Animal { var name: String //抽象字段 var age: Int // 抽象字段 var color: String = "black" //普通字段 def cry() //抽象方法 def sayOk(): Unit = { //普通方法 println("ok") } } //快捷键 alt+enter class Mouse extends Animal { //override 有两层含义 1. 重写(override 必须保留) 2. 实现 (可以省略) override var name: String = _ override var age: Int = _ override def cry(): Unit = { } }
注意
- 抽象类不能被实例化
- 抽象类不一定要包含 abstract 方法。也就是说,抽象类可以没有 abstract 方法
- 一旦类包含了抽象方法或者抽象属性,则这个类必须声明为 abstract
- 抽象方法不能有主体,不允许使用 abstract 修饰。
- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法和抽象属性,除非它自己也声明为 abstract 类。
- 抽象方法和抽象属性不能使用 private、final 来修饰,因为这些关键字都是和重写/实现相违背的。
- 抽象类中可以有实现的方法.
- 子类重写抽象方法不需要 override,也可以写
继承层级
- Scala 中,所有其他类都是 AnyRef 的子类,类似 Java 的 Object。
- AnyVal 和 AnyRef 都扩展自 Any 类。Any 类是根节点/根类型
- Any 中定义了 isInstanceOf、asInstanceOf 方法,以及哈希方法等。
- Null 类型的唯一实例就是 null 对象。可以将 null 赋值给任何引用,但不能赋值给值类型的变量
- Nothing 类型没有实例。它对于泛型结构是有用处的,举例:空列表 Nil 的类型是 List[Nothing],它是 List[T] 的子类型,T可以是任何类
伴生对象
类 class 的 object
在伴生对象中定义 apply 方法,可以实现: 类名(参数) 方式来创建对象实例, 而不用 new
在伴生对象中定义 unapply 方法,可以实现:模式匹配的提取器
在伴生对象中定义 applySeq 方法,可以实现:模式匹配的序列提取器(一次提取多个)
特质 trait
[code]trait 特质名 { trait体 }
- trait 命名 一般首字母大写.
- Scala 中,java中的接口可以当做特质使用
- object T1 extends Serializable Serializable: 就是 scala 的一个特质
- 如果有多个特质或存在父类,那么需要采用 with 关键字连接
- 没有父类
class 类名 extends 特质1 with 特质2 with 特质3 . - 有父类
class 类名 extends 父类 with 特质1 with 特质2 with 特质3
动态混入
动态混入可以在不影响原有的继承关系的基础上,给指定的类扩展功能, 解耦接入.
构建对象的同时如果混入多个特质,称之为叠加特质,那么特质声明及构建顺序从左到右,方法执行顺序从右到左。
- Scala 中特质中如果调用 super,并不是表示调用父特质的方法,而是向前面(左边)继续查找特质,如果找不到,才会去父特质查找
- 如果想要调用具体特质的方法,可以指定:super[特质].xxx(…).其中的泛型必须是该特质的直接超类类型
[code]//在声明类的时候混入特质, 对该类所有的对象有效, 在混入特质时,该对象还没有创建 //在声明对象的时候使用with混入多个特质, 特质中的方法对该方法有效, 可以调用, 实际是构造匿名子类, 可 //以理解成在混入特质时,对象已经创建了 { //1.E... //2. A... //3. B.... // 4. C.... // 5. D.... // 6. F.... val ff = new FF //1. E... //2. K.... //3. A //4. B //5. C //6. D println("第2种构建顺序...") val ff2 = new KK with CC with DD } trait AA { println("A...") } trait BB extends AA { println("B....") } trait CC extends BB { println("C....") } trait DD extends BB { println("D....") } class EE { println("E...") } class FF extends EE with CC with DD { println("F....") } class KK extends EE { println("K....") }
- 特质可以继承类,以用来拓展该类的一些功能
[code]trait LoggedException extends Exception{ def log(): Unit ={ println(getMessage()) // 方法来自于Exception类 } }
- 所有混入该特质的类,会自动成为那个特质所继承的超类的子类
[code]trait LoggedException extends Exception{ def log(): Unit ={ println(getMessage()) // 方法来自于Exception类 } }//UnhappyException 就是Exception的子类. class UnhappyException extends LoggedException{ // 已经是Exception的子类了,所以可以重写方法 override def getMessage = "错误消息!" }
- 如果混入该特质的类,已经继承了另一个类(A类),则要求A类是特质超类的子类,否则就会出现了多继承现象,发生错误
自身类型
自身类型(self-type):主要是为了解决特质的循环依赖问题,同时可以确保特质在不扩展某个类的情况下,依然可以做到限制混入该特质的类的类型
[code]//Logger 就是自身类型特质 trait Logger { // 明确告诉编译器,我就是 Exception,如果没有这句话,下面的 getMessage 不能调用 this: Exception => def log(): Unit ={ // 既然我就是 Exception, 那么就可以调用其中的方法 println(getMessage) } } //因为 Logger 使用自身类型,要求继承(混入)Logger 必须是 Exception 子类 // class AAA extends Logger [错误] class BBB extends Exception with Logger //ok
嵌套类
- 类共有五大成员 属性 方法 构造器 代码块 内部类, 内部类最大的特点就是可以直接访问私有属性
- 从定义在外部类的成员位置上来看,
1) 成员内部类(没用static修饰)
2) 和静态内部类(使用static修饰)
- 定义在外部类局部位置上(比如方法内)来看:
分为局部内部类(有类名)
匿名内部类(没有类名)
[code]//创建内部类对象 val innerClass1 = new outer1.ScalaInnerClass // 访问方式:外部类名.this.属性名 //可以访问方法 ScalaOuterClass.this.sayHi
在内部类中访问外部类的属性和方法两种方法:
- 方式1
内部类如果想要访问外部类的属性,可以通过外部类对象访问。
即:访问方式:外部类名.this.属性名 - 方式2
内部类如果想要访问外部类的属性,也可以通过外部类别名访问(推荐)。
即:访问方式:外部类名别名.属性名 【外部类名.this 等价 外部类名别名】
[code]class ScalaOuterClass { myouter => class ScalaInnerClass2 { //成员内部类 def info() = { //使用别名的方式来访问外部类的属性和方法,相当于myouter是一个外部类的实例ScalaOuterClass2.this //这时需要将外部类的属性和方法的定义/声明放在别名后 println("name = " + myouter.name + " age =" + myouter.sal) //可以访问方法 myouter.sayHi } } var name: String = "scott" private var sal: Double = 1.2 private def sayHi(): Unit = { println("say hi~~~哈哈~~~") } }
类型投影
在方法声明上,如果使用 外部类#内部类 的方式,表示忽略内部类的对象关系,等同于 Java 中内部类的语法操作,我们将这种方式称之为 类型投影(即:忽略对象的创建方式,只考虑类型)
如何解决问题呢?
使用类型投影!
- Scala入门系列(七):面向对象之继承
- Scala入门系列(五):面向对象之类
- Scala入门系列(六):面向对象之object
- Scala入门系列(八):面向对象之trait
- Scala快跑系列【面向对象入门】
- scala入门教程:scala中的面向对象定义类,构造函数,继承
- scala入门(4) 对象比较
- java基础--面向对象入门
- js中的面向对象入门
- [.net 面向对象程序设计进阶] (6) Lamda表达式(二) 表达式树快速入门
- 面向对象,你入门了吗?
- js入门三JavaScript 面向对象实现
- python 面向对象入门 - 之 单元测试
- Java基础(2):Java面向对象入门和基本语法
- python面向对象入门
- 【php】php面向对象入门级别的类
- OC基础语法<1.3> 入门面向对象语法-self、new、 @property和synthesize
- scala入门-08 apply方法和单例对象的使用
- scala 开发入门(5)-- 类与对象
- java面向对象入门之创建类