Kotlin 进阶之路(三) 面向对象
3.1 面向对象的概念
面向对象是将要解决的问题按照一定的规则划分为多个独立的对象,然后通过调用对象的方法来解决
问题。面向对象的三大特性,封装性、继承性、多态性。
1.封装性
封装性是面向对象的核心思想,将对象的属性和行为封装起来,不需要让外界知道具体的实现细节。
例如,用户使用电脑,只需要手指敲键盘即可,无需知道电脑内部是怎么工作的。
2.继承性
继承性主要是指类与类之间的关系,通过继承,可对原有类的方法进行扩展。
例如,学生和工人都属于人类,人类都需要吃饭,学生和工人都可以继承人类,分别扩展各自吃饭
的方法。
3.多态性
多态性是指在程序中允许出现重名,在一个类中定义的属性和方法被其它类继承后,它们可以有
不同的数据类型或表现出不同的行为,这使得同一个属性和方法在不同的类中具有不同语义。
例如,当听到 Cut 这个单词时,理发师的行为是理发,演员的行为是停止表演,不同的对象,
所表现的行为是不一样的。
3.2 类与对象
- 类的定义
类是对象的抽象,用于描述一组对象的共同特征和行为。类中可以定义成员变量(属性)
和成员函数(方法)
class Student{ //成员变量 private val name = "" private val age = 0 //成员函数 private fun sayHi(){ println("你好,我是${name}, 我今年${age}岁") } }
- 对象创建
Kotlin 中对象通过 类名() 的形式来直接创建
var 对象名称 = 类名()
var stu = Student()
对象的成员变量访问方式
对象引用.对象成员
class Student{ //成员变量 var name = "Mike" var age = 16 //成员函数 fun sayHi(){ println("你好,我是${name}, 我今年${age}岁") } } fun main(args: Array<String>) { var student = Student() student.sayHi() println("姓名:${student.name}") student.name = "James" println("姓名:${student.name}") /*你好,我是Mike, 我今年16岁 姓名:Mike 姓名:James*/ }
- 类的封装
类的封装是指在定义一个类时,将类中的属性私有化,使用 private 关键字来修饰,私有
属性只能在所属类中访问,为了让外界能范围私有属性,可以提供 public 修饰的共有方法。
class Student{ var name : String = ""//默认是公有 private var age : Int = 0 fun setAge(age : Int){ if (age > 0) this.age = age else println("输入年龄有误") } 默认是公有 fun sayHi(){ println("你好,我是${name}, 我今年${age}岁") } } fun main(args: Array<String>) { var stu = Student() stu.setAge(24) stu.name = "Kobe" stu.sayHi() /*你好,我是Kobe, 我今年24岁*/ }
3.3 构造函数
构造函数是类的一个特殊成员,将在类实例化对象时被自动调用,可用来为类的属性赋值。
Kotlin 中的构造函数分为两种————主构函数和次构函数。构造函数使用关键字 constructor 定义
- 主构函数
在 Kotlin 中,一个类可以有一个主构造函数和多个次构造函数。主构造函数位于类头跟
在类名之后,如果主构造函数没有任何注解或可见性修饰符, constructor 关键字可
省略。语法格式如下
class 类名 constructor([形参1, 形参2, 形参3]){}
当定义一个类时,如果没有显示指定主构函数, Kotlin 编译器会默认为其生成一个无参
主构函数,这点和 Java 是一样的。无参主构函数的两种写法。
class 类名 constructor(){}
class 类名(){}
一般我们经常用的是有参的构造函数为属性赋值。在主构函数中赋值时,使用 init{} 初始化代码块
class Click constructor(username : String){ var name : String init { name = username println("我是$username") } } fun main(args: Array<String>) { var click = Click("猫哆哩") //我是猫哆哩 }
- this 关键字
Kotlin 中也提供了 this 关键字,用于在函数中访问对象的其它成员。上面的 init{} 可写成
init{
this.name = username
}
- 次构函数
Kotlin 中可以定义多个次构函数,次构函数必须调用主构函数或其它次构函数,调用方式
为 次构函数 : this(参数列表)
注意:
当新定义的次构函数调用主构函数或次构函数时,被调用的构造函数中参数顺序必须和新定义
的次构函数参数顺序一致,并且参数个数必须小于新定义的次构函数的参数个数。
class Workers constructor(name : String){ var name : String init { this.name = name println("我叫${name}") } constructor(name: String, age: Int) : this(name){ println("我叫${name}, 我今年${age}岁") } constructor(name: String, age: Int, sex: String) : this(name, age){ println("我叫${name}, 我今年${age}岁,我是${sex}生") } } fun main(args: Array<String>) { var person = Workers("洛天依", 26, "女") /*我叫洛天依 我叫洛天依, 我今年26岁 我叫洛天依, 我今年26岁,我是女生*/ }
3.4 类的继承
- 类的继承
在 Kotlin 中,类的继承是指在一个现有类的基础上去构建一个新类,构建出来的新类被
称为子类,现有类被称作父类,子类将自动拥有父类所有可继承的属性和方法。
继承使用关键字 : , class A : B{} A 类继承 B 类
Kotlin 中所有的类默认使用 final 修饰,因此,当继承某类时,需要在类前面加 open 关键字
open class Father(){ fun sayHello(){ println("Hello") } } class Son : Father(){} fun main(args: Array<String>) { var son = Son() son.sayHello() //Hello }
总结:
1、在 Kotlin 中,一个类只能继承一个父类,不能继承多个父类
2、多个类可以继承一个父类
3、在 Kotlin 中,多层继承是允许的,即一个类的父类可以再去继承另外的父类
4、在 Kotlin 中,子类和父类是一种相对概念,一个类可以是另一个类的子类,也可能是其它类的父类
- 方法重写
子类对父类的方法或属性进行修改,该过程被称为方法或属性的重写。
注:
重写的方法或属性应和父类的方法名或属性名一样,并且方法前面使用 override 关键字标识
在父类中被重写的属性或方法前必须使用 open 关键字修饰
open class Father(){ open var name = "洛天依" open var age = 26 open fun sayHello(){ println("Hello! 我叫$name, 我今年$age 岁") } } class Son : Father(){ override var name = "阿黛尔" override var age = 48 override fun sayHello() { println("Hello! It's me, 我叫 $name, 我今年$age 岁") } } fun main(args: Array<String>) { var father = Father() father.sayHello() var son = Son() son.sayHello() /*Hello! 我叫洛天依, 我今年26 岁 Hello! It's me, 我叫 阿黛尔, 我今年48 岁*/ }
- super 关键字
super 关键字用于访问父类的成员变量或方法,一般用于在子类中访问父类的成员
super.成员变量
super.成员方法([形参1, 形参2…])
注:
Kotlin 中所有类都继承 Any 类,它是所有类的父类,类比 Java 中的 Object类
如果一个类在声明时没有指定父类,则默认为 Any 类,在程序运行时, Any 类自动映射
为Java 中的 java.lang.Object 类
3.5 抽象类和接口
- 抽象类
抽象类使用关键字 abstract 修饰,抽象方法也需要用 abstract 修饰。需要注意的是,包
含抽象方法的类必须声明为抽象类,但抽象类可以不包含任何抽象方法,只需用 abstract
修饰即可。抽象类是不可以被实例化的,其中的抽象方法没有方法体,不可以被调用。
abstract class Animal{ abstract fun eat() } class Monkey(food : String) : Animal(){ var food = food override fun eat() { println("猴子正在吃$food") } } fun main(args: Array<String>) { var monkey = Monkey("香蕉") monkey.eat() //猴子正在吃香蕉 }
- 接口
如果一个抽象类中的所有方法都是抽象的,则可以将类定义为接口。接口也就是一个特
殊的抽象类,使用关键字 interface 来声明,接口中定义的方法默认包含 abstract 修饰
符,可以省略不写
interface Animal{ fun eat() } interface Monkey : Animal{ fun sleep() } class GoldenMonkey(food: String) : Monkey{ var food = food override fun eat() { println("我是金丝猴,我喜欢吃$food") } override fun sleep() { println("我是金丝猴,我喜欢睡觉") } } fun main(args: Array<String>) { var goldenMonkey = GoldenMonkey("香蕉") goldenMonkey.eat() goldenMonkey.sleep() /*我是金丝猴,我喜欢吃香蕉 我是金丝猴,我喜欢睡觉*/ }
总结:
接口中的方法都是抽象的,不能实例化对象
当一个类实现接口时,如果这个类是抽象类,则实现接口中部分方法即可,否则需要实现所有方法
一个类可以实现多个接口,但 : 之间需要用 , 隔开
一个接口可以继承多个接口,但 : 之间需要用 , 隔开
一个类在继承另一个类的同时还可以实现接口,继承的类和实现的接口都放在 : 后面
3.6 常见类
- 嵌套类、内部类
Kotlin 的嵌套类是指可以嵌套在其他类中的类,该类不能访问外部类的成员
Kotlin 的内部类是指使用 inner 修饰的嵌套类,可以访问外部类的成员
class Outer{ var name = "洛天依" var age = 26 class Nested{ fun sayHello(){ //println("Hello! 我叫${name}, 今年${age}岁")//无法访问外部类字段 } } } class Outer{ var name = "洛天依" var age = 26 inner class Inner{ fun sayHello(){ println("Hello! 我叫${name}, 今年${age}岁") } } } fun main(args: Array<String>) { Outer().Inner().sayHello() }
- 枚举类
枚举就是一 一例举,每个枚举常量都是一个对象,枚举常量用逗号分隔,用关键字 enum 修饰
enum class Week1{ 星期一,星期二,星期三,星期四,星期五,星期六,星期日 } enum class Week2(val what:String, val doSomeThine: String){ MONDAY("星期一", "上班") TUESDAY("星期二", "聚会") WEDNEWSDAY("星期三", "上班") THURSDAY("星期四", "上班") FRIDAY("星期五", "上班") SATURDAY("星期六", "加班") SUNDAY("星期日", "休息") }
- 密封类
密封类用于表示受限制的类层次结构,当一个值只能在一个集合中取值,而不能取其他
值时,此时可以用密封类。在某种意义上,密封类是枚举类的扩展,即枚举类型的值集合。每个枚举常量只存在一个实例,而密封类的一个子类可以有包含状态的多个实例。
密封类必须用 sealed 关键字修饰
由于密封类的构造函数是私有的,因此密封类的子类只能定义在密封类的内部或同一文件中
sealed class Stark{ //罗伯 斯塔克 class RobStark : Stark(){} //桑莎 斯塔克 class SansaStark : Stark(){} //艾丽娅 斯塔克 class AryaStark : Stark(){} //嵌套类 class BrandonStark(){} } //密封类 Stark 子类 class JonSnow : Stark(){}
注:
Kotlin 中密封类和枚举类的区别,密封类适用于子类可数的情况,而枚举类适用实例可数的情况
- 数据类
Kotlin 中专门处理数据一些数据或对象的状态的类称为数据类,类似于 Java 中的 Bean
类、entity 类、model 类。数据类的语法格式为,
data class 类名({形参1, 形参2…})
注:
数据类的主构造函数至少有一个参数,如果需要一个无参的构造函数,可将构造函数的参数都设置默认值
数据类中的主构造函数中传递的参数必须用 val 或 var 修饰
在 Kotlin 1.1 版本之前数据类只能实现接口,1.1 版本后可继承其它类
编译器可自动生成一些常用的方法,如 equals()、hashCode()、toString()、
componentN()、copy()等,这些方法也可自定义
- 单例模式
单例模式是指在程序运行期间针对该类只存在一个实例,就好比世界上只有一个太阳一
样。Kotlin 中的单例模式是通过 object 关键字来完成的,通过 object 修饰的类即为单例类。
object Singleton{ var name = "单例模式" fun sayHello(){ println("Hello, 我是一个$name,浑身充满正能量") } } fun main(args: Array<String>) { Singleton.name = "小太阳" Singleton.sayHello() //Hello, 我是一个小太阳,浑身充满正能量 }
注: 单例类不需要创建实例对象,直接通过 类名.成员名 调用类中的属性或函数
- 伴生对象
Kotlin 中没有静态变量,因此使用伴生对象来替代 Java 中的静态变量,伴生对象是在类
加载是初始化,生命周期与该类的生命周期一致。通过关键字 companion 来标识,由于
每个类中有且仅有一个伴生对象,因此可以不指定伴生对象的名称,其它对象也可共享
伴生对象。
companion object 伴生对象名称(也可以不写){
程序代码...
}
有名称: 调用方式为 类名.伴生对象名.成员名 或 类名.成员名
无名称: 调用方式为 类名.Companion.成员名 或 类名.成员名
class Company{ companion object Factory{ fun sayHello(){ println("我是一个伴生对象,与伴生类相伴") } } } fun main(args: Array<String>) { //第一种调用方式 类名.伴生对象名.成员函数名 Company.Factory.sayHello() //第二种调用方式 类名.成员函数名 Company.sayHello() }
3.7 委托
委托也叫代理模式,是最常用的一种设计模式。简单说就是 A 的工作交给 B 来做。
在 Kotlin 中,委托是通过 by 关键字实现的,主要分为类委托,和属性委托。
- 类委托
接下来演示定义接口 Wash,已经两个接口的实现类 Child 和 Parent, Parent 类要实现的功能委托给 Child 进行处理。
interface Wash{ fun washDishes() } class Child : Wash{ override fun washDishes() { println("委托大头儿子洗碗,耶!") } } //第一种委托方式 class Parent : Wash by Child(){} //第二种委托方式 class Parent(washer : Wash) : Wash by washer fun main(args: Array<String>) { //第一种 var parent = Parent() parent.washDishes() //第二种 var child = Child() Parent(child).washDishes() //委托大头儿子洗碗,耶! }
- 属性委托
Kotlin 中的属性委托是指一个类的某个属性值不是在类中直接进行定义,而是将其委托给一个代
理类,从而实现对该类的属性进行统一管理。语法格式如下:
val/var <属性名> : <类型> by <表达式>
by 后面的表达式是指委托类,属性对应的 get() 和 set() 会被委托给 getValue() 和
setValue() 方法,因此属性的委托不必实现任何接口。
class Parent{ var money : Int = 0 operator fun getValue(child: Child, property : KProperty<*>) : Int{ println("getValue()方法被调用,修改的属性为: ${property.name}") return money } operator fun setValue(child: Child, property: KProperty<*>, value: Int){ println("getValue()方法被调用,修改的属性为: ${property.name} 属性值: ${value}") money = value } } class Child{ //将压岁钱委托给父母 var money : Int by Parent() } fun main(args: Array<String>) { val child = Child() println("(1) 父母给孩子100元压岁钱") child.money = 100 println("(2) 买玩具花了50") child.money -= 50 println("(3) 自己还剩${child.money}") /*(1) 父母给孩子100元压岁钱 getValue()方法被调用,修改的属性为: money 属性值: 100 (2) 买玩具花了50 getValue()方法被调用,修改的属性为: money getValue()方法被调用,修改的属性为: money 属性值: 50 getValue()方法被调用,修改的属性为: money (3) 自己还剩50*/ }
注:
setValue() 和 getValue() 方法前必须用 operator 关键字修饰
getValue()方法返回类型必须与委托属性相同或是其子类
如果委托属性是 val 类型,被委托方法只需要实现 getValue(), 如果是 var 类型,则需要实现
getValue() 和 setValue()
- 延迟加载
Kotlin 中提供了延迟加载功能,又叫懒加载,当变量被访问时才会被初始化。通过关键字
by lazy 标识,延迟加载要求变量声明为 val 类型。延迟加载也是委托的一种形式。
fun main(args: Array<String>) { val content by lazy { println("Hello") "world"//第一次初始化后,再次调用该变量时,只会输出 ^lazy 的内容 } println(content) println(content) /*Hello world world*/ }
注:
延迟加载的变量在第一次初始化时会输出代码块中的所有内容,之后再次调用时,都只会
输出最后一行代码的内容。