Kotlin的面向对象

类与对象

类是对象的抽象,用于描述一组对象的共同特征和行为。

类中可以定义成员变量和成员函数,其中:

成员变量用于描述对象的特征,也被称作属性;

成员函数用于描述对象的行为,可以简称为函数方法

同时顶级属性顶级方法,指的是该属性或方法放在类的最外层,和java中的全局变量类似。

Kotlin 编译器会将所有的顶层方法全部编译成静态方法,因此只要你定义了一 个顶层方法,那么它就一定是静态方法。 ​​​​​​​

构造函数

在实例化对象的同时,就为这个对象的属性进行赋值,可以通过构造函数来实现,因为构造函数是类的一个特殊成员,他会在类实例化的时候被自动调用。

Kotlin的构造函数分为两种:主构函数、次构函数。构造函数使用constructor关键字进行修饰,一个类可以有一个主构函数,多个次构函数。

主构函数

主构函数位于类头,在类名之后。若主构函数没有任何注解,或可见性修饰符(如public),constructor关键字可省略。主构函数的定义语法如下:

class 类名 constructor ([形参1,形参2,形参3]){}

若在定义类时没有指定主构函数,Kotilin将会与java一样,自动生成一个无参主构函数。

class 类名 constructor(){}    //第一种写法
class 类名 (){}               //第二种写法,省略constructor,即普通类的定义形式

在主构函数赋值时,通常使用init{}初始化代码块。 

class Clerk constructor(username: String) {
    var name: String

    init {
        name = username
        println("My name is $name")
    }
}

fun main() {
    var clerk = Clerk("bob")
}
运行结果

次构函数 

在实际赋值时,有可能出现多种情况,比如只给name赋值,或者同时给name和age赋值,这时候只有主构函数还不够,就需要次构函数来完成。

而一个次构函数,必须要调用主构函数,或者其他的次构函数,调用方法为“次构函数:this(参数列表)”。

需要注意的是,新定义的次构函数,在调用主构函数或者其他次构函数时,被调用的函数,参数顺序必须与新定义的顺序一致,同时参数个数必须小于新定义的次构函数。

class Workers constructor(name: String) {
    var name: String

    init {
        this.name = name
        println("My name is $name")
    }

    constructor(name: String, age: Int) : this(name) {//继承了主构函数
        println("My name is $name, I am $age years old")
    }

    constructor(name: String, age: Int, sex: String) : this(name, age) {//继承了次构函数
        println("My name is $name, I am $age years old, I am a $sex")
    }
}

fun main() {
    var workers = Workers("bob", 10, "male")
}
运行结果

在编写构造函数时,通过this关键字不难看出,第一个次构函数,调用的是主构函数,而第二个次构函数,调用的则是第一个次构函数。

在main函数中,在初始化workers变量时,传入了三个变量,此时将进入第二个次构函数。而因为上述的调用关系,在初始化时,三个构造函数都将被投入使用。

类的继承 

类的继承

Kotlin中,所有类都默认使用final关键字修饰,所以想要继承某个类时,需要在这个类前面加上open关键字。在继承时,Kotlin使用英文冒号来修饰。

open class Father() {
    fun sayHello() {
        println("Hello")
    }
}

class Son() : Father() {}

fun main() {
    var son = Son()
    son.sayHello()
}
运行结果

在示例中,类Father使用了open关键字修饰,使得可以被其他类继承;而类Son继承了Father,虽然在类Son中没有任何内容,但是可以使用类Father中的方法sayHello(),说明在继承时,子类会继承父类的所有方法。 

 在Kotlin中,继承有以下几种情况:

  1. 一个类只能继承一个父类。
  2. 一个类可以拥有多个子类。
  3. 多层继承是允许的。比如b继承了a,但b同时可以是c的父类,此时c也可以称作是a的子类。

方法重写(override)

在子类中,重写的属性需要用override来修饰。

同样的,在父类中,被重写的属性需要用open来修饰。

open class Father() {
    open var name = "bob"
    open var age = 35
    open fun sayHello() {
        println("My name is $name, I am $age years old")
    }
}

class Son() : Father() {
    override var name = "bobby"
    override var age = 5
    override fun sayHello() {
        println("My name is $name, I am son of ${super.name}, I am $age years old")
    }
}

fun main() {
    var father = Father()
    father.sayHello()
    var son = Son()
    son.sayHello()
}
运行结果

运行结果中,子类虽然重写了父类中的sayHello方法,但是在运行时,只会走各自类中的sayHello方法,子类并不会调用父类的方法。

super关键字

当子类重写父类方法后,子类对象无法访问父类被重写的方法。为此,Kotlin中可以使用super关键字,让子类对象来调用父类方法。

open class Father() {
    open var name = "bob"
    open var age = 35
    open fun sayHello() {
        println("My name is $name, I am $age years old")
    }
}

class Son() : Father() {
    override var name = "bobby"
    override var age = 5
    override fun sayHello() {
        super.sayHello()
        println("My name is $name, I am son of ${super.name}, I am $age years old")
    }
}

fun main() {
    var son = Son()
    son.sayHello()
}
运行结果

 Any类与Object类

Kotlin中,所有类都继承Any类,他是所有列的父类。若一个类在声明时没有指定父类,则默认其父类为Any类。

在运行时,Any类会自动映射为java中的java.lang.Object类。

fun main(){
    println(Any().javaClass)//输出结果为:class java.lang.Object
}

在Java中,有8种基本类型和引用类型。在Kotlin中,所有类型都是引用类型,这些引用类型统一继承父类Any。Any类中提供了3个方法,分别如下:

方法名方法作用
equals()检测两个对象是否相等
hashCode()返回一个对象的哈希码值
toString()返回一个对象的字符串形式

相对应的,在Java中,Object类是所有类的父类,但不包括8种基本类型(如Int、Long

、Double等),Object中提供了11个方法,分别如下:

方法名方法作用
equals()检测两个对象是否相等
hashCode()返回一个对象的哈希码值
toString()返回一个对象的字符串形式
getClass()返回一个Class类型的对象
clone()创建并返回一个对象的副本,也就是复制该对象
finalize()Object类的子类可以覆盖该方法以实现资源清理工作,垃圾收集时,由对象上的垃圾收集器调用该方法
notify()唤醒一个等待该对象的线程
notifyAll()唤醒在这个对象监视器上等待的所有线程
wait()使当前线程等待,直到另一个线程被调用
wait(long)使当前线程等待,直到另一个线程被调用,该方法中的参数是等待的最大时间,单位是毫秒
wait(long,int)使当前线程等待,直到另一个线程被调用,该方法中的第一个参数是等待的最大时间,单位是毫秒;第二个参数是额外时间,以毫微秒为单位,范围是0~999999

抽象类和接口(interface)

抽象类

定义一个类时,通常需要定义一些方法,来描述该类的行为特征。

但有时这些方法的实现方法无法确定,此时可以将其定义为抽象方法,使用abstract关键字来修饰,当要使用这个抽象方法时,需要实现该方法体;

而当一个类中包含了抽象方法,这个类就必须被定义为抽象类,同样使用的是abstract关键字。

//定义抽象类和抽象方法的语法格式
abstract class Animal{
    abstract fun eat()
}

虽然包含抽象方法的类必须被声明为抽象类,但抽象类可以不包含任何抽象方法。

同时,抽象类不能被实例化,因为其中可能含有抽象方法,而抽象方法是不包含方法体的,所以不能被调用。如果要调用抽象类中的抽象方法,需要创建一个子类,在子类中将其实现。

abstract class Animal() {
    abstract fun eat()
}

class Monkey(food: String) : Animal {
    var food = food
    override fun eat() {
        println("Monkey is eating $food")
    }
}

fun main() {
    var monkey = Monkey("banana")
    monkey.eat()//运行结果:Monkey is eating banana
}

当子类实现了父类的抽象方法,可以正常进行实例化,并通过该实例化对象调用实现的方法。 

接口

若一个抽象类中的所有方法都是抽象的,则可以将这个类用接口来定义。

因此,接口也是一种特殊的抽象类。

interface Animal{
    fun eat()//定义抽象方法
}

在使用接口时,并不像抽象方法那样,需要使用abstract关键字来修饰,这是因为接口中已经默认包含该关键字。

由于接口中的所有方法都是抽象方法,所以不能简单地通过实例化对象来调用接口中的方法,而是要定义一个类来实现接口中的所有方法。

interface Animal {
    fun eat()
}

class Monkey(food: String) : Animal {
    var food = food
    override fun eat() {
        println("Monkey is eating $food")
    }
}

fun main() {
    var monkey = Monkey("banana")
    monkey.eat()//运行结果:Monkey is eating banana
}

在程序中,还可以定义一个接口去继承另一个接口,通过冒号(:)实现。

interface Animal {
    fun eat()
}

interface Monkey : Animal {
    fun sleep()
}

class GoldenMonkey(food: String) : Monkey {
    var food = food
    override fun eat() {
        println("I am golden monkey, I love to eat $food")
    }

    override fun sleep() {
        println("I am golden monkey, I love to sleep")
    }
}

fun main() {
    var goldenMonkey = GoldenMonkey("banana")
    goldenMonkey.eat()
    goldenMonkey.sleep()
}
//运行结果:
//I am golden monkey, I love to eat banana
//I am golden monkey, I love to sleep

有几点需要注意:

  1. 当一个类实现接口,如果这个类是抽象类,则不用实现该接口中的全部方法,否则将要全部实现;
  2. 当一个类使用冒号(:)实现接口,可以实现多个接口,被实现的多个接口使用逗号(,)隔开。一个接口在继承多个接口时,也是使用以上的格式;
  3. 一个类在继承另一个类的同时,也可以实现接口,被继承的类名和实现的接口名字,都放在冒号(:)后面,也是使用逗号(,)隔开。

常见类

嵌套类

嵌套在其它类中的类,该类不能访问外部类的成员。

在没有任何修饰的情况下,定义在一个类内部的类,被默认成为嵌套类。

class Outer {
    var name = "bob"
    var age = 35

    class Inner {
        fun sayHello() {
            println("My name is $name, I am $age years old")//会报错,因为无法访问外部类成员
        }
    }
}

内部类(inner)

同样是定义在其他类中的类。

与嵌套类的区别在于,被inner关键字修饰,以此修饰的内部类可以访问外部类成员。

class Outer {
    var name = "bob"
    var age = 35

    inner class Inner {
        fun sayHello() {
            println("My name is $name, I am $age years old")
        }
    }
}

fun main() {
    Outer().Inner().sayHello()
}

在main方法中访问内部类的成员时,需要先实例化外部的类,在实例化内部的类,最终调用内部类的方法。 

与java的区别是,在java中,将一个类定义在另一个类的内部,则将这个类称为成员内部类;如果加上static修饰,则是静态内部类。java 的成员内部类可以访问外部类的所有成员。

枚举类(enum)

顾名思义就是一一例举,每个枚举常量都是一个对象,使用逗号分隔,用enum关键字修饰。

由于每个枚举常量都是枚举类的实例,因此这些实例也可以初始化,同时枚举支持构造函数。

enum class Week1{
    星期一,星期二,星期三,星期四,星期五,星期六,星期日
}

enum class Week2{
    MONDAY("星期一","上班"),
    TUESDAY("星期二","聚会"),
    WEDNEWSDAY("星期三","上班"),
    THURSDAY("星期四","上班"),
    FRIDAY("星期五","上班"),
    SATURDAY("星期六","加班"),
    SUNDAY("星期日","休息")
}

密封类(sealed)

当一个值只能在一个集合中取值,而不能取其他值时,此时可以使用密封类。

在某种意义上,密封类是枚举类的拓展。每个枚举类只能存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。

由于密封类的构造函数是私有的,因此密封类的子类只能定义在密封类的内部,或同一个文件中。

sealed class Stark {
    class RobStark : Stark() {}        //密封类的子类
    class SansaStark : Stark() {}      //密封类的子类
    class AryaStark : Stark() {}       //密封类的子类
    class BrandonStark() {}            //密封类的嵌套类
}

class JonSnow : Stark() {}             //不在嵌套类中的嵌套类子类

密封类用于子类可数的情况,而枚举类用于实例可数的情况。 

数据类(data)

专门用来保存数据或者对象状态的类,被称为数据类。

在java中,这种类一般会称为bean类、entity类或model类。

data class 类名([形参1,形参2……])//语法格式

 需要注意以下几点:

  1. 数据类的主构函数至少有一个参数,如果需要一个无参的构造函数,可以将其中的参数直接设置成默认值;
  2. 数据类中的主构函数中传递的参数必须用val或var修饰;
  3. 数据类不可以用abstract、open、sealed或inner关键字修饰。
data class Man(var name: String, var age: Int) {}

fun main() {
    var man = Man("bob", 20)
    println("man : $man")//运行结果:man : Man(name=bob, age=20)
}

 根据运行结果可知,man中的数据已存储在数据类Man中。

单例模式(object)

指的是在程序运行期间,针对该类只能存在一个实例。

就好比世界只有一个太阳,现在要设计一个太阳类,这个类就只能有一个实例对象。

object Singleton {
    var name = "单例模式"
    fun sayHello() {
        println("Hello! I am the $name, I love this world")
    }
}

fun main() {
    Singleton.name = "Sun"
    Singleton.sayHello()//运行结果:Hello! I am the Sun, I love this world
}

从上述代码不难看出,在使用单例类时,不需要再去创建一个该类的实例化对象,而是直接使用“类名.成员名”的形式调用类中的方法与参数。

这是因为通过object关键字创建单例类时,默认创建了该类的单例对象。

伴生对象(companion)

在Kotlin中没有静态变量,因此使用了伴生对象来替代。

伴生对象在类加载时初始化,与类的生命周期相同,每个类有且仅有一个伴生对象,因此可以不指定伴生对象的名称,并且其他类可以共享伴生对象。

companion object 伴生对象名称(可省略){//伴生对象语法    
    代码……
}

由于伴生对象可以不指定名称,因此在调用时同样有两种方式。

class Company {
    companion object Factory{
        fun sayHello(){
            println("I am a companion")
        }
    }
}

fun main() {
    Company.Factory.sayHello()    //第1种调用方式:类名.伴生对象名.成员函数名
    Company.sayHello()            //第2种调用方式:类名,成员函数名
}

委托(by)

委托模式也称代理模式,简而言之是将a的工作交给b来做。

类委托

委托是有两个对象完成的,因此类委托也包含两个对象:委托类和被委托类。

在委托类中,没有真正的功能方法,该类的功能是通过被委托类中的方法实现的。

第一种委托方式:定义了一个接口Wash以及两个实现接口的类Child和Parent,但是在Parent中没有实现方法,它需要实现的功能交给了Child来完成。 

//第一种委托方式
interface Wash {
    fun washDishes()
}

class Child : Wash {
    override fun washDishes() {
        println("委托儿子洗碗")
    }
}

class Parent : Wash by Child() {}

fun main() {
    var parent = Parent()
    parent.washDishes()
}

第二种委托方式:在委托类继承接口时,传入一个被委托类的实例,通过被委托类调用其中的方法。当在main方法中调用时,初始化一个实例并传入即可。

//第二种委托方式
interface Wash {
    fun washDishes()
}

class Child : Wash {
    override fun washDishes() {
        println("委托儿子洗碗")
    }
}

class Parent(washer: Wash) : Wash by washer {}//传入一个被委托类实例

fun main() {
    var child = Child()//初始化一个被委托类实例,用于传入委托类
    Parent(child).washDishes()
}

属性委托

指一个类中的属性不是在类中直接定义,而是委托给一个代理类,在其中对所有属性统一管理。

val/var <属性名>: <类型> by <委托类>//语法格式

 由于属性对应的get和set方法会被委托给getValue和setValue方法,因此属性的委托不必实现任何接口。对于val类型的属性,只需提供getValue方法。

以下代码的实例,参照的是过年小朋友收到红包,将压岁钱委托给家长。

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("setValue()方法被调用,修改的属性:${property.name}、属性值:${value}")
        money = value
    }
}

class Child() {
    var money: Int by Parent()//将压岁钱委托给家长
}

fun main() {
    val child = Child()
    println("(1)父母给孩子100元压岁钱")
    child.money = 100
    //运行结果:setValue()方法被调用,修改的属性:money、属性值:100
    println("(2)买玩具花了50")
    child.money -= 50
    //运行结果:
    //getValue()方法被调用,修改的属性:money
    //setValue()方法被调用,修改的属性:money、属性值:50
    //getValue()方法被调用,修改的属性:money
    println("(3)自己还剩${child.money}")
}

若Child类没有委托给Parent类,将使用自己的get和set方法,但是委托后,通过输出可知使用的是Parent类的getValue和setValue方法。 

注意以下几点:

  1. getValue和setValue方法前必须使用operator关键字;
  2. getValue方法的返回类型必须与委托属性相同,或者是其子类。

延迟加载(by lazy)

在Kotlin中,在声明变量或者属性的同时,需要对其初始化,否则会报错。

能不能在变量被使用的时候,再初始化?

为此Kotlin提供了延迟加载(又称懒加载),当变量被访问时,才会初始化。

这样的好处显而易见,因为不是把变量一股脑全员初始化,可以提高效率,使得程序启动更快。

延迟加载的变量必须被声明为val,即不可变变量,相当于java中的final。

val 变量: 变量类型 by lazy{
    变量初始化代码
}

延迟加载的变量,在第一次初始化时输出所有内容,但是在之后,只会输出最后一行。 

fun main() {
    val content by lazy {
        println("Hello")
        "World"
    }
    println(content)
    //运行结果:
    //Hello
    //World
    println(content)
    //运行结果:
    //World
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值