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()
对象的成员变量访问方式
对象引用.对象成员
stu.name
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*/
}
注:
延迟加载的变量在第一次初始化时会输出代码块中的所有内容,之后再次调用时,都只会
输出最后一行代码的内容。