5、类和对象

在之前,我们一直在使用顶层定义

val a = 20   //直接在kt文件中定义变量

fun message() {   //直接在kt文件中定义函数
    println("我是测试方法")
}

而学习了类之后,这些内容也可以定义到类中,作为类的属性存在。

类的定义与对象创建

class Student {
    //在没有任何内容时,花括号可以省略
}

除了直接在某个.kt文件中直接编写之外,为了规范,我们一般将一个类单独创建一个文件,也就是类名称与创建的类文件是一模一样的。

构造函数

主要构造函数

构造函数也是函数的一种,但是它是专用于对象的创建,Kotlin中的类可以添加一个主构造函数和一个或多个次要构造函数。主构造函数是类定义的一部分,像下面这样编写:

class Student constructor(name: String,age: Int) {
}

如果主构造函数没有任何注释或可见性修饰符,则可以省略constructor关键字,如果类中没有其他内容要写,可以直接省略花括号,最后就变成这样了:

class Student(name: String,age: Int)

次要构造函数

除了直接使用主构造函数创建对象外,我们也可以添加一些次要构造函数,比如我们的学生可以只需要一个名字就能完成创建,我们可以直接在类中编写一个次要构造函数

如果该类有一个主构造函数,则每个次要构造函数需要通过另一个次要构造函数直接或间接委托给主构造函数。委托到同一类的另一个构造函数是this关键字完成的

class Student(var name: String, var age: Int){
    // 次要构造
    constructor(name:String) : this(name, 0)
}

如果一个类没有主构造函数,那么我们也可以直接在在类中编写次要构造函数,但是不需要主动委托一次主构造函数,他这里会隐式包含

class Student{
    var name: String = ""
    var age: Int = 0
    constructor(name: String,age: Int){
        this.name = name
        this.age = age
    }
}

主构造函数和次要构造函数区别

属性

但是,上面仅仅是定义了构造函数的参数,这还不是类的属性,我们可以为这些属性添加varval关键字来表示这个属性是可变还是不变的

class Student(var name: String, val age: Int)

这样才算是定义了类的属性,我们也可以给这些属性设置初始值

class Student(var name: String, val age: Int = 18)  //默认每个学生18岁

除了将属性添加到构造函数中,我们也可以将这些属性直接作为类的成员变量写到类中,但是这种情况必须要配一个默认值,否则无法通过编译

class Student {
    var name: String = ""   //必须配一个默认值
    var age: Int = 0
}

这样我们就可以不编写主构造函数也能定义属性,但是这里仍然会隐式生成一个无参的构造函数,为了构造函数能够方便地传值初始化,也可以像这样写

class Student(name: String, age: Int) {
    var name: String = name   //通过构造函数传递过来
    var age: Int = age
}

懒加载

当然,如果不希望这些属性在一开始就有初始值,而是之后某一个时刻去设定初始值,我们也可以为其添加懒加载

class Student{
    lateinit var name: String
    var age: Int = 0
}

getter 和 setter

像这样编写的类成员变量,也可以自定义对应的getter和setter属性

class Student{
    var name: String = ""
        get() {
            return field
        }
        set (value) {
            field = value
        }
    var age: Int = 0
        get() {
            return field
        }
        set(value) {
            field = value
        }
}

对象的创建

跟我们调用普通函数一样,这里的函数名称就是类的名称,如果一个类没有编写构造函数,那么这个类默认情况下使用一个无参构造函数创建

var s = Student()

如果是有构造函数的类,我们只需要填写需要的参数即可,调用之后,类的属性就是这里我们给进去的参数了

var s = Student("小明", 18)

可以使用.来获取和修改对象的属性

fun main() {
    var s = Student()
    s.name = "张三"
    s.age = 18
    println("name:${s.name},age:${s.age}")
}

对象的初始化

在对象创建时,我们可能需要做一些初始化工作,我们可以使用初始化代码块来完成,初始化代码块使用init关键字来完成。

比如由于主构造函数无法编写函数体,但是我们需要主构造做一些初始化的逻辑,这个时候就可以使用init

class Student(var name: String,var age: Int){
    init {
        if(age < 18){
            age = 18
        }
    }
}

存在多个初始化操作时,从上往下按顺序执行

class Student(var name: String,var age: Int){
    init {
        println("第一个init")
    }
    init {
        println("第二个init")
    }
}

需要注意一下,次要构造函数实际上需要先执行主构造函数,而在执行主构造函数时,会优先将初始化代码块执行

class Student(var name: String,var age: Int){
    init {
        println("第一个init")
    }
    init {
        println("第二个init")
    }
    constructor(name: String):this(name,18){
        println("次要构造函数")
    }
}
//第一个init
//第二个init
//次要构造函数

类的成员函数

class Student(var name: String,var age: Int){
    fun sayHello(){
        println("Hello, my name is $name, and I am $age years old.")
    }
}

fun main() {
    var s = Student("张三",18)
    s.sayHello()
}

如果函数有和类属性名称相同的参数,如果函数中的变量存在歧义,那么优先使用作用域最近的一个,比如函数形参的name作用域更近,那么这里的name拿到的一个是形参name,而不是类的成员属性name。

class Student(var name: String, var age: Int) {
    //此时函数的参数也有一个name变量,而类的成员也有一个name属性
    fun hello(name: String){
        //这里得到的name是方法的形参,而不是类属性
        println("大家好啊,我叫$name,今年$age岁了")
      	//如果我们需要获取的是类中的成员属性,需要使用this关键字来表示当前类
      	println("大家好啊,我叫${this.name},今年$age岁了")
    }
}

在类中,我们同样可以定义多个同名但不同参数的函数实现重载

class Student(private var name: String, private var age: Int) {
    fun hello() = println("大家好啊,我叫${this.name},今年${age}岁了")
    fun hello(gender: String) = println("大家好啊,我叫${this.name},今年${age}岁了,性别${gender}")
}

运算符重载

Kotlin支持为程序中已知的运算符集提供自定义实现,这些运算符具有固定的符号表示(如+*)以及对应的优先级,要实现运算符重载,请为相应类型提供具有对应运算符指定名称的成员函数,而当前的类对象,则直接作为对应运算符左边的操作数,如果是一元运算符(比如++自增运算符,只需要身)则直接作为操作数参与运算。

其实 Kotlin 本身就有很多类型重载了运算符,例如 Int 类型的变量,plus方法就是重载的运算符,定义如下

public operator fun plus(other: Long): Long

这个函数添加了一个operator关键字,这其实是运算符重载,能够自定义运算符实现的功能,我们之前使用这些数字进行运算,比如加减乘除,实际上都是这些基本类型在类中重载了运算符实现的。

我们可以为自定义类重载运算符

fun main() {
    var p1 = Point(1.0,2.0)
    var p2 = Point(3.0,4.0)
    var p3 = p1 + p2
    println("p3.x = ${p3.x},p3.y = ${p3.y}")
}

class Point(var x: Double,var y: Double){
    // 重载加号运算符
    operator fun plus(other: Point): Point{
        return Point(x + other.x,y + other.y)
    }
}

运算符对应函数名称

一元运算符

符号 对应的函数名称
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a-- a.dec()
a++ a.inc()

二元运算符

符号 对应的函数名称
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)
a..<b a.rangeUntil(b)

对于in这种运算,必须返回Boolean类型的结果

符号 对应的函数名称
a in b b.contains(a)
a !in b !b.contains(a)

自增简化运算符

这类运算符都是将运算结果赋值给左边的操作数,比如a = a + b等价于a += b

符号 对应的函数名称
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

比较运算符

所有比较都会转换为compareTo函数调用,此函数返回Int值,这个值用于和 0 比较判断是否满足条件。

运算符 对应的函数名称
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

小括号

运算符 对应的函数名称
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ..., i_n) a.invoke(i_1, ..., i_n)

中括号

运算符 对应的函数名称
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ..., i_n] a.get(i_1, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)

中缀函数

实际上中缀函数在我们之前很多时候都有出现,比如位运算

println(i shl 1)

这里的shl并不是一个运算符,而是一段自定义的英文单词,像这种运算符是怎么做到的呢?

这其实是中缀函数,用infix关键字标记的函数被称为中缀函数,在使用时,可以省略调用的点和括号进行调用,Infix函数必须满足以下要求:

fun main() {
    var s = Student("张三", 18)
    println(s isAge 18)
}

class Student(var name: String,var age: Int) {

    infix fun isAge(age: Int): Boolean {
        return this.age == age
    }
}

中缀函数调用的优先级低于算术运算符、类型转换和rangeTo运算符,例如以下表达式就是等效的:

另一方面,infix函数调用的优先级高于布尔运算符&&||is-和in-checks以及其他一些运算符的优先级。这些表达式也是等价的:

同时,如果需在类中使用中缀函数,必须明确函数的调用方(接收器)比如

class MyStringCollection {
  
    infix fun add(s: String) { /*...*/ }

    fun build() {
        this add "abc"   // 正确
        add("abc")       // 正确
        //add "abc"        // 错误: 没有指定调用方或无法隐式表达
    }
}

解构声明

有时候,我们在使用对象时可能需要访问它们内部的一些属性

fun main() {
    val student = Student("小明", 18)
    println(student.name)
    println(student.age)
}

利用解构,可以直接得到Student对象内部的name和age熟悉作为变量使用

fun main() {
    var s = Student("张三", 18)
    var (name,age) = s
    println("name:$name,age:$age")
}

class Student(var name: String,var age: Int) {
    // 声明结构出来的每一个变量对应的属性
    operator fun component1() = name
    operator fun component2() = age
}

结构同样也适用于Lambda 表达式

// a,b 就是从 Student 中结构出来的
val func2: (Student, Int) -> Unit = { (a, b), i ->
    println("名字: $a, 年龄: $b")
    println(i)
}

访问权限控制

有些时候,我们可能不希望别人使用我们的所有内容。

在类、对象、接口、构造函数和函数,以及属性上,可以为其添加 可见性修饰符 来控制其可见性,在Kotlin中有四个可见性修饰符,它们分别是:privateprotectedinternalpublic,默认可见性是public,在使用顶级声明时,不同可见性的访问权限如下:

private fun inner(){
    //我们不希望这个函数能够在其他地方被调用
}

在类中定义成员属性时,不同可见性的访问权限如下:

class Student(private var name: String, //name属性无法被外部访问,因为是私有的
              internal var age: Int) {  //age可以被外部访问,但是无法再其他项目中访问到
  
    private constructor() : this("", 10)  //这个无参构造无法被外部访问,因为是私有的
}

封装、继承和多态

类的封装

封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个程序带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要使用Getter和Setter函数来查看和设置变量。

class Student(private var name: String, private var age: Int) {
    fun getName(): String = name
    fun getAge(): Int = age
}

我们甚至还可以将主构造函数改成私有的,需要通过其他的构造函数来构造

class Student private constructor(private var name: String, private var age: Int) {
    constructor() : this("", 18)
}

类的继承

在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,根据前面的访问权限等级,子类可以使用父类中所有非私有的成员。

在Kotlin中,我们可以使用继承操作来实现这样的结构,默认情况下,Kotlin类是“终态”的(不能被任何类继承)要使类可继承,需要用open关键字标记需要被继承的类

Kotlin 的类只能 单继承

open class Student(var name: String,var age: Int)

class ArtStudent(var artLevel: Int, name: String, age: Int): Student(name, age)

以上这样是在继承的时候指定调用父类的构造函数,如果父类有多个构造函数,那么可以使用super来指定调用哪个构造函数

open class Student(var name: String,var age: Int){

    constructor(name: String): this(name, 0)

    constructor(): this("",0)
}

class ArtStudent: Student{

    var score: Int = 0

    constructor(score: Int): super("Tom",18){
        this.score = score
    }

}

如何理解上面这一堆写法:

优先级关系:父类初始化 - 子类主构造 - 子类辅助构造

属性的覆盖

我们可以希望子类继承父类的某些属性,但是我们可能希望去修改这些属性的默认实现

我们可以使用override关键字来表示对于一个属性的重写(覆盖)

open class Student {
  	//注意,跟类一样,函数必须添加open关键字才能被子类覆盖
    open fun hello() = println("我会打招呼")
}

class ArtStudent : Student() {
    fun draw() = println("我会画画")
  	//在子类中编写一个同名函数,并添加override关键字,我们就可以在子类中进行覆盖了,然后编写自己的实现
    override fun hello() = println("哦哈哟")
}

属性也是一样

open class Student {
    open val name: String  = "大明"
    fun hello() = println("我会打招呼,我叫: $name")
}

//在主构造函数中覆盖,也是可以的,这样会将构造时传入的值进行覆盖
class ArtStudent(override val name: String) : Student() {
    fun draw() = println("我会画画")
}

重写父类方法之后,但是还需要调用父类方法,就可以使用super关键字来调用

open class Student {
    open fun hello() = println("我会打招呼")
}

class ArtStudent : Student() {
    fun draw() = println("我会画画")
    override fun hello() {   //覆盖父类函数
        super.hello()   //使用super.xxx来调用父类的函数实现,这里super同样表示父类
        println("哦哈哟")  //再写自己的逻辑
    }
}

类的多态

fun main() {
    var s: Student = ArtStudent(1, "Tom", 18)
}

open class Student(var name: String,var age: Int)

class ArtStudent(var artLevel: Int, name: String, age: Int): Student(name, age)

顶层Any类

在我们不继承任何类的情况下,实际上Kotlin会有一个默认的父类,所有的类默认情况下都是继承自Any类的。

这个类的定义如下

/**
 * Kotlin类继承结构中的根类. 所有Kotlin中的类都会直接或间接将Any作为父类
 */
public open class Any {
    /**
     * 判断某个对象是否"等于"当前对象,这里同样是对运算符"=="的重载,而具体判断两个对象相等的操作需要由子类来定义
     * 在一些特定情况下,子类在重写此函数时应该保证以下要求:
     * * 可反身: 对于任意非空值 `x`, 表达式 `x.equals(x)` 应该返回true
     * * 可交换: 对于任意非空值 `x` 和 `y`, `x.equals(y)` 当且仅当 `y.equals(x)` 返回true时返回true
     * * 可传递: 对于任意非空值 `x`, `y`, 和 `z`, 如果 `x.equals(y)` 和 `y.equals(z)` 都返回true, 那么 `x.equals(z)` 也应该返回真
     * * 一致性: 对于任意非空值 `x` 和 `y`, 在多次调用 `x.equals(y)` 函数时,只要不修改在对象的“equals”比较中使用的信息,那么应当始终返回同样的结果
     * * 永不等于空: 对于任意非空值 `x`, `x.equals(null)` 应该始终返回false
     */
    public open operator fun equals(other: Any?): Boolean

    /**
     * 返回当前对象的哈希值,它具有以下约束:
     * 
     * * 对同一对象多次调用该函数时,只要不修改对象上的equals比较中使用的信息,那么此函数就必须始终返回相同的整数 
     * * 如果两个对象通过`equals`函数判断为true,那么这两个对象的哈希值也应该相同
     */
    public open fun hashCode(): Int

    /**
     * 将此对象转换为一个字符串,具体转换为什么样子的字符串由子类自己决定
     */
    public open fun toString(): String
}

抽象类

有些情况下,我们设计的类可能仅仅是作为给其他类继承使用的类,而其本身并不需要创建任何实例对象

//使用abstract表示这个是一个抽象类
abstract class Student {
    abstract val type: String  //抽象类中可以存在抽象成员属性
    abstract fun hello()   //抽象类中可以存在抽象函数
  	//注意抽象的属性不能为private,不然子类就没法重写了
}

当一个子类继承自抽象类时,必须要重写抽象类中定义的抽象属性和抽象函数:

class ArtStudent: Student() {
    override val type: String = "美术生"
    override fun hello() = println("$type")
}

当然,抽象类不仅可以具有抽象的属性,同时也具有普通类的性质,同样可以定义非抽象的属性或函数

abstract class Student {
    abstract val type: String
    abstract fun hello()
    fun test() = println("不会有人玩到大三了才开始学Java吧")  //定义非抽像属性或函数,在子类中不强制要求重写
}

接口

接口只能包含函数或属性的定义,所有的内容只能是abstract的,它不像类那样完整。

Kotlin 的接口是 多实现

interface Student {
  
    var name: String
    
    fun sayHello()
}

接口中的函数可以具有默认实现,默认情况下是open的,除非private掉

interface Student {
    
    var name: String

    fun sayHello() = println("Hello $name")
}

实现接口的方式和继承一样,直接写到后面,多个接口用逗号隔开

class Student: A,B{
    override fun a() {   
    }

    override fun b() {   
    }
}

interface A{
    fun a()
}

interface B{
    fun b()
}

如果实现的多个接口有相同的函数,那么就只需要实现一次。但是要使用接口的默认实现,那么就需要使用super关键字指定。

class Student: A,B{
    override fun test() {
        super<A>.test()
    }
}

interface A{
    fun test() = println("A")
}

interface B{
    fun test() = println("B")
}

类的扩展

Kotlin提供了扩展类或接口的操作,而无需通过类继承或使用装饰器等设计模式,来为某个类添加一些额外的函数或是属性,我们只需要通过一个被称为扩展的特殊声明来完成。

需要注意的是,我们在不同包中定义的扩展属性,同样会受到访问权限控制,需要进行导入才可以使用

扩展方法

比如我们想为String类型添加一个自定义的方法test

fun String.myTest() = this.length

fun main() {
    var s = "Tom"
    println(s.myTest())
}

扩展属性

直接var String.myValue: String = "1234"这样扩展属性是不允许的,因为扩展并不是真的往类中添加属性,因此,扩展属性本质上也不会真的插入一个成员字段到类的定义中,这就导致并没有变量去存储我们的数据。

我们只能明确定义一个getter和setter来创建扩展属性,才能让它使用起来真的像是类的属性一样。

var String.myLength: Int
    get() = this.length
    set(value) {}