# Kotlin教程 - 8 面向对象

# 8.4 继承

在现实世界,有狗和猫,它们都属于动物类,狗、猫和动物类的关系是父类和子类的关系。

狗和猫都会叫,我们可以在狗类中定义一个叫的方法,在猫类中定义一个叫的方法,但是这两个叫的方法是一样的,都是用嘴叫。我们可以在动物类中定义一个叫的方法,让狗和猫类都继承这个动物类,那么它们就拥有了叫的方法,就不用再定义了。

# 1 继承的语法

和 Java 不同,在 Kotlin 中,一个类默认是不能被继承的,除非使用 open 关键字来修饰

Kotlin 中只支持单继承,也就是一个类只能继承一个类,不能同时继承多个类。

举个栗子:

我们先定义一个父类——动物类,定义两个子类——狗、猫类:

open class Animal(var name: String) {

    fun makeSound() {
        // 定义了一个叫的方法
        println("我是${this.name},我会叫")
    }
}

class Dog(name: String, var age: Int) : Animal(name) {
    // 定义一个狗类,继承自动物类
}

class Cat(name: String, var age: Int) : Animal(name) {
    // 定义一个猫类,继承自动物类
}

fun main() {
    val dog = Dog("Doubi", 5) // 创建一个狗对象
    dog.makeSound()     // 我是Doubi,我会叫

    val cat = Cat("Niubi", 10) // 创建一个猫对象
    cat.makeSound()      // 我是Niubi,我会叫
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在上面的代码中,我们先定义了一个动物类,然后在动物类中定义了 name 属性 ,并定义了一个 makeSound 方法;

然后定义狗类,继承动物类,那么就拥有了动物类的属性和方法。猫类也同样。

因为 Animal 类主构造函数有一个参数,所以继承动物类必须传递这个参数,但是在 Dog 类的主构造函数中,name 不能使用 var 或 val 声明,因为这样就会创建属性,和父类中的属性一样,就冲突了。

当然,调用父类构造可以传递参数, 也可以直接写死,例如:

class Dog(name: String, var age: Int) : Animal("Dog") {
}
1
2

这样,所有的狗都叫 "Dog" 了,一般不会这么做。

继承注意点:

继承不能继承父类私有的成员变量和方法。

如果父类的成员变量和属性不想被子类继承,可以设置为私有成员。

# 2 重写父类方法

继承父类就拥有了父类非私有的成员变量和方法,如果父类不是我想要的方法,我还可以进行覆盖。

例如,动物类提供了叫的方法,但是我狗有自己的叫法,我要汪汪叫,那么我们可以重写父类的方法,实现自己的个性化。

和 Java 不同,Kotlin中父类的方法默认是不能被子类覆盖重写的,除非父类的方法使用 open 关键字修饰,子类重写的时候还需要使用 override 关键字。

举个栗子:

open class Animal {
    // 定义一个动物类 
    var name = ""

    open fun makeSound() {          // 需要使用open修饰,子类才能重写
        // 定义了一个叫的方法
        println("我是${this.name},我会叫")
    }
}

class Dog : Animal() {
    // 定义一个狗类,继承自动物类
    override fun makeSound() {      // 子类重写要添加override关键字
        println("我是${this.name},我会汪汪叫")
    }
}

class Cat : Animal() {
    // 定义一个猫类,继承自动物类
}

fun main() {
    val dog = Dog()
    dog.name = "Doubi"
    dog.makeSound()         // 我是Doubi,我会汪汪叫

    val cat = Cat()
    cat.name = "Niubi"
    cat.makeSound()          // 我是Niubi,我会叫
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

父类方法使用 open 关键字修饰才能被重写,子类重写父类方法,需要添加 override 关键字。

在上面的代码中,狗类重写了父类的方法,有了自己的个性化处理;猫类没有重写父类的方法,还是使用了父类的方法。

那么如果我在父类中添加一个 eat() 方法,使用这个方法来调用 makeSound() 方法,然后子类方法调用 eat() 方法,那么会如何执行呢?

open class Animal {
    var name = ""

    open fun makeSound() {          // 需要使用open修饰,子类才能重写
        println("我是${this.name},我会叫")
    }

    open fun eat() {
        makeSound()
    }
}

class Dog : Animal() {
    override fun makeSound() {      // 子类重写要添加override关键字
        println("我是${this.name},我会汪汪叫")
    }
}

class Cat : Animal() {
}

fun main() {
    val dog = Dog()
    dog.name = "Doubi"
    dog.eat()         // 我是Doubi,我会汪汪叫

    val cat = Cat()
    cat.name = "Niubi"
    cat.eat()          // 我是Niubi,我会叫
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

dog.eat() 调用的是继承自父类的方法,在 eat() 方法中调用了 makeSound() 方法,那么调用的是父类的makeSound() 方法还是子类的makeSound() 方法呢?

是调用子类的方法!子类继承自父类相当于将父类的属性和方法复制了一份到子类中,并对其中的方法重写,所以调用的都是子类的方法。

# 3 覆盖父类属性

和重写父类方法一样,父类的属性使用 open 修饰,子类才能覆盖,而且覆盖的时候也要使用 override 关键字。

重写父类的方法,是因为子类想有个性化的实现,覆盖父类属性有什么作用呢,父类的属性子类都可以使用啊。

一般在子类中覆盖父类的属性,是想在属性的 getset 中添加额外的逻辑处理,例如在获取属性值时做一些验证或者转换。

举个栗子:

open class Animal {
    open var age: Int = 0
        get() {
            println("Getting name from Animal")
            return field
        }
        set(value) {
            println("Setting name in Animal")
            field = value
        }
}

class Dog : Animal() {
     override var age: Int = 0		// 覆盖父类的属性
        get() {
            println("Getting age from Dog")
            return if (field < 0) 0 else field		// 修改获取的逻辑
        }
        set(value) {
            println("Setting age in Dog")
            field = value
        }
}

fun main() {
    val dog = Dog()
    dog.age = -2
    println(dog.age)  // 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

在上面的代码中,在子类中覆盖了父类的属性,针对 get 方法进行了修改,变更了父类中的实现逻辑。

# 4 调用父类属性和方法

我们在重写父类方法的时候,应该尽量做到在父类方法的基础上进行扩展。

所以很有可能需要调用父类被重写的方法,然后在父类原来的方法上进行扩展。

那么如果我们想要在子类中调用父类被重写的方法呢?

使用 super 关键字来调用父类的方法。

举个栗子:

open class Animal {
    open var name = "动物"

    open fun makeSound() {
        println("我是${this.name},我会叫")
    }
}

class Dog : Animal() {
    override var name = "狗"

    override fun makeSound() {
        println("我原来是${super.name}, 我现在是${this.name},我会汪汪叫")	// 调用父类属性和子类属性
    }

    fun test() {
        super.makeSound()  // 使用super调用父类的方法
        this.makeSound()   // 使用this调用子类的方法
    }
}

fun main() {
    val dog = Dog()
    dog.test()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

执行结果:

我是狗,我会叫 我原来是动物, 我现在是狗,我会汪汪叫

如果要在子类中调用父类的属性和方法,可以通过 super.属性 和 super.父类方法() 来调用。可以使用 this 来调用子类的方法。

但是在上面的例子中,使用 super.makeSound() 调用父类的方法,父类方法中的 this.name 访问的还是子类的属性。

# 5 构造函数的继承

子类继承父类,是没办法直接继承构造函数的,但是子类的构造函数最终都需要调用父类的构造函数。

下面我总结了三条规则,可以参考一下:

  1. 如果子类有主构造函数(子类类名后面有括号),那么子类的主构造函数必须显式的调用父类的主构造或次要构造函数(子类继承父类,父类名后面要传递参数)。子类的次构造函数必须调用子类的主构造函数。

举个栗子:

open class Animal(val name: String) {
    constructor(name: String, age: Int): this(name) {
        
    }
}

// 如果子类有主构造函数
class Dog(name: String) : Animal(name, 12) {  // 调用父类的主构造函数或次要构造函数
    constructor(name: String, age: Int): this(name) {   // 通过this调用主构造函数
        
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
  1. 如果子类没有主构造函数,有次要构造函数,那么子类继承父类,不能在父类名后面传递参数,需要在次要构造函数后,使用 super 关键字调用父类的主构造或次要构造函数

举个栗子:

open class Animal(val name: String) {
    constructor(name: String, age: Int): this(name) {

    }
}

// 如果子类没有主构造函数
class Dog : Animal {  
    constructor(name: String, age: Int): super(name) {  // 子类需要调用父类的主构造函数或次要构造函数

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
  1. 如果子类没有主构造函数(子类类名后面没有括号),也没有次要构造函数,那么子类继承父类,需要在父类名后传递参数来调用父类的主构造或次要构造函数

举个栗子:

open class Animal(val name: String) {
    constructor(name: String, age: Int): this(name) {

    }
}

// 如果子类没有主构造函数
class Dog : Animal("Hello", 12) {  // 调用父类的主构造函数或次要构造函数
}
1
2
3
4
5
6
7
8
9

# 6 类型转换

我们可以判断某个对象的类型,以及是否是某个对象的实例。

首先有如下代码:

open class Animal {
    // 动物类
}

class Dog : Animal() {
    // 狗类,继承自动物类
}

class Cat : Animal() {
    // 猫类,继承自动物类
    fun makeSound() {
        println("我会喵喵叫")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 获取类型

kotlin中,可以使用 ::class.simpleName 获取变量的类型的名称,注意:子类的对象不是父类的对象的类型。

举个栗子,基于上面的代码:

fun main() {
    val cat = Cat()
    val dog1: Dog = Dog()
    val dog2: Animal = Dog()
    println(cat::class.simpleName) // 输出: Cat
    println(dog1::class.simpleName) // 输出: Dog
    println(dog2::class.simpleName) // 输出: Dog
}
1
2
3
4
5
6
7
8

# is 关键字

is 关键字可以判断某个对象是否是某个类型的实例,子类对象也是父类对象的实例。

举个例子,基于上面的代码:

fun main() {
    val animal: Animal = Cat()		// 这个地方要声明为Animal类型,否则下面的cat is Dog会报错

    println(animal is Cat) 				// true,猫对象是猫类型的实例
    println(animal is Animal) 		// true,猫对象是动物类的实例
    println(animal is Dog) 				// false,猫对象不是狗类的实例
}
1
2
3
4
5
6
7

# 类型转换

其实在前面也已经讲过,如何如何将 Any 类型转换为其他类型。这里重新演示一下。

使用 as 关键字可以进行类型转换,但是在转换之前必须要进行判断,否则会报错。

fun main() {
    val animal: Animal = Cat()
  	// animal.makeSound()		// 这里的代码会报错,因为是Animal类型,Animal类型中没有makeSound()方法

    if (animal is Cat) {
        val cat = animal as Cat
        cat.makeSound()
    }
}
1
2
3
4
5
6
7
8
9

其实经过 is 判断后,在 if 代码块中已经自动进行了类型转换了。所以上面可以不使用 as 关键字进行转换,直接调用:

fun main() {
    val animal: Animal = Cat()

    if (animal is Cat) {
        animal.makeSound()			// 直接调用
    }
}
1
2
3
4
5
6
7

# 7 Any类

在 Kotlin 中,Any 是所有类的超类,类似于 Java 中的 Object

一个类如果没有写明继承哪个类,那么默认都是继承自Any 类,所以所有的类都是直接或间接继承 Any 类,Any 类是所有类的父类!

一个类,虽然没有定义 equalstoString 等方法,但都有这些方法,这是因为继承自 Any 类的。

Any 类包含一些通用的方法,例如:

  1. equals(): 用于比较两个对象是否相等,如果子类没有重写,默认使用的是 === 比较,也就是比较的是地址;
  2. hashCode(): 返回对象的哈希码值。
  3. toString(): 返回对象的字符串表示形式。
  4. javaClass: 获取对象的运行时类引用。

你可以使用 Any 类型来表示任何 Kotlin 对象。例如:

class Student{}

fun main() {
    val anyObject: Any = "This is a string"
    val anotherObject: Any = 42
    val stu: Any = Student()

    println(anyObject.toString())       						// This is a string
    println(anotherObject.javaClass.simpleName)     // Integer
    println(stu)    				// Student@7c30a502 结果不固定,为类名 + hashCode的16进制
    println(stu.hashCode()) // 2083562754,转换为16进制为7c30a502
}
1
2
3
4
5
6
7
8
9
10
11
12

在这个例子中,anyObject 是一个字符串类型的对象,anotherObject 是一个整数类型的对象。它们都被声明为 Any 类型,因此可以调用 Any 类中定义的通用方法。

Any 类的主要作用是作为 Kotlin 类型系统的根,所有类都直接或间接继承自它。这使得在 Kotlin 中可以统一对待不同类型的对象,可以在不确定对象类型的情况下使用 Any 类型来表示。

# 8.5 访问控制修饰符

我们在前面的类中,对于类中私有的属性,可以使用 private 关键字来修饰,这样在类外面就无法访问私有属性了。

访问标识控制符用于控制类、接口、属性、方法等成员在其他代码中的可见性。它们帮助开发者限制或开放特定部分的代码,提供更好的封装和模块化。

Kotlin 提供了四种访问标识控制符,范围从大到小介绍:

# 1 public

public 是默认的修饰符,表示成员对所有代码可见。

在 Kotlin 中,如果不指定任何可见性修饰符,默认为 public

# 2 internal

internal 修饰符表示在同一个模块可见。

什么是同一个模块呢?

你看下面我们的项目,它只有一个模块,就是 hello-kotlin,那么 internal 修改的类或函数,在这整个模块内都是可以访问的。

有的项目是包含多个模块的,那么 internal 修改的类或函数就不能跨模块访问了。

# 3 protected

protected 成员可以在类内部和其子类中访问。

和 Java 不同,Kotlin 中的 protected 并不允许包内其他类访问该成员。

open class MyBaseClass {
    protected val protectedValue = 10
}

class MyDerivedClass : MyBaseClass() {
    fun accessProtectedValue() {
        println(protectedValue) 	// 可以在子类中访问父类的属性
    }
}
1
2
3
4
5
6
7
8
9

如果属性或方法是 publicinternalprotected 修饰的,子类都是可以访问的。

# 4 private

private 修饰的成员,如果是在类内部使用,那么在类外部无法访问这些 private 成员。

如果是在类外部使用,那么只能在当前文件内可以访问。

package com.doubi.domain

class Student(val name: String, var age: Int) {
    private fun good() {
        hello()     // 可以调用同一个包下的函数
    }
}

private fun hello() {		// 私有函数
    println("Hello World!")
}

fun main() {
    val stu = Student("Doubi", 12)
    //stu.test()      // 报错,类外无法调用私有方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5 总结

修饰符 访问范围
private 类内部私有,只能外类内部访问;类外部私有,只能在当前文件内访问
protected private + 子类可见
internal 对同一模块内的代码可见
public 是默认的修饰符,对所有地方可见。

# 8.6 面向对象扩展

# 1 定义伴生对象

在前面我们定义的属性和方法,是实例变量和实例方法,也叫成员变量和成员方法。

成员变量对于每个实例而言,是独立的数据,每个对象之间相互不会影响。创建一个对象,就会开辟独立的内存空间保存对象的实例变量数据。但是无论创建多少对象,成员方法只有一份,所有对象共享,哪个对象调用方法就是操作哪个对象的成员变量。

我们还可以在类中定义一些数据,是所有对象共享的,在 Java 中,我们可以通过静态属性和静态方法来定义属于类的数据和方法。但是在 Kotlin 中没有静态属性和方法,但是可以使用伴生对象来模拟静态变量和方法。

打个比方,我们定义了一个Student类,然后通过Student类来创建对象,我们想知道一共创建了多少个Student对象,应该如何操作呢?

我们可以通过定义伴生对象来实现:

举个栗子:

/**
 * 定义类
 */
class Student(var name: String, var age: Int) {
    // 定义伴生对象
    companion object {
        // 定义属性
        var stuCount: Int = 0
        // 定义方法
        fun getCount():Int {        
            return stuCount
        }
    }

    init {
        stuCount ++
    }
}

fun main() {
    var stu1 = Student("Doubi", 12)
    var stu2 = Student("Niubi", 13)
    println(Student.stuCount)                   // 伴生对象中的属性,通过类型可以直接调用
    println(Student.Companion.stuCount)         // 也可以通过类名.Companion.属性来调用

    Student.stuCount = 5
    println(Student.getCount())             // 伴生对象中的方法,通过类名可以直接调用
    println(Student.Companion.getCount())   // 也可以通过类名.Companion.方法来调用
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

在上面的代码中,我们定义了一个Student类,然后在类中使用 companion object 定义了伴生对象,在伴生对象中定义了属性和方法,伴生对象中的属性和方法是属于类的,而不是属于实例的。所以访问的时候通过 类名.属性类名.Companion.属性 就可以访问到伴生对象中的属性,方法也是一样的。

在上面的代码中,在每次创建对象的时候,都会执行 init 代码块,伴生对象中的变量 stuCount 就会加1。从上面的代码可以看出,在类中可以直接访问到伴生对象中的属性。

其实在类中可以定义和伴生对象中相同名称的属性和方法。

举个栗子:

/**
 * 定义类
 */
class Student(var name: String, var age: Int) {
    var stuCount:Int = 0

    // 定义伴生对象
    companion object {
        // 定义属性
        var stuCount: Int = 0
        // 定义方法
        fun getCount():Int {        
            return stuCount
        }
    }

    init {
        stuCount++;             // 访问的是成员变量
        Companion.stuCount ++   // 访问的是伴生对象中的属性
    }

    // 成员方法,和伴生对象中的方法同名
    fun getCount():Int {        
        return stuCount
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

成员变量和方法可以和伴生对象中的属性和方法同名。名称相同时,如果在类中访问,可以使用 Companion.属性 来访问伴生对象中的属性,方法也是一样的。

需要注意,在伴生对象中是没有办法访问类中的成员变量和方法的,因为伴生对象的方法是通过类来调用的,而如果能调用成员方法,没办法确定访问的是哪个对象的数据,所以是无法操作的。

# 2 单例对象

在上面的伴生对象其实和 Java 中的单例模式有一些相似。

什么是单例模式呢?

单例模式是一种设计模式,旨在确保在整个应用程序中只存在一个实例。正常创建一个类,我们可以通过这个类创建无数的实例对象。而使用单例模式,我们创建一个类后,使用这个类只能创建一个实例对象。

那么为什么要用到单例模式呢?

在整个项目中可能有一些全局的配置,日志记录等功能,这种全局存在一个管理对象就可以了,不用创建多个,所以会使用单例模式来实现。

在 Kotlin 中我们可以使使用 object 关键字来创建单例对象,确保在应用程序中只存在一个实例。

举个栗子:

/**
 * 定义单例对象
 */
object MySingleton {
    // 定义属性
    val host:String
    val port:Int

    init {
        println("初始化配置")
        host = "192.168.1.1"
        port = 3306
    }

    fun printInfo() {
        println("主机:${host}, 端口:${port}")
    }
}

fun main() {
    MySingleton.printInfo()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在上面的代码中使用 object 关键字创建了单例对象,并在对象中创建了属性和方法。单例类中的属性和方法可以通过类名直接访问。

需要注意,单例对象不能有构造函数。

# 3 定义工具类

在 Kotlin 中,一般使用单例对象来定义工具类。

例如下面就定义了一个字符串操作的工具类。

/**
 * 定义字符串工具类
 */
object StringUtils {
    fun isEmpty(input: String): Boolean {           // 判断字符串是否为空
        return input == null || input.isEmpty()
    }

    fun isNotEmpty(input: String): Boolean {        // 判断字符串是否不为空
        return !isEmpty(input)
    }

    fun trim(input: String): String {               // 去掉字符前后的空格
        if (isEmpty(input)) return input
        return input.trim()
    }
}

fun main() {
    val name = " Doubi "
    println(StringUtils.isEmpty(name))
    println(StringUtils.isNotEmpty(name))
    println(StringUtils.trim(name))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在上面的代码中使用 object 关键字定义了一个伴生类,其中的方法都是静态类方法,可以使用类名来直接调用。通常定义工具类都使用这种方式来定义。

# 4 对象表达式

在 Java 中,尤其在 Android 开发中,经常会用到匿名内部类,就是通过接口或抽象类创建只使用一次的类和对象。例如组件的OnClickListener 的回调方法。

在 Kotlin 中,对于这种用完就丢的类实例,可以使用对象表达式来实现。

open class OnclickListener {
    open fun onclick() = "onclick view"
}

fun main() {
    val listener = object : OnclickListener() {		// 通过OnclickListener创建一个匿名内部类对象
        override fun onclick(): String {		// 重写方法
            return "override method"
        }
    }

    println(listener.onclick())		
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在上面的代码中,通过 object 关键字创建匿名内部类的对象,并重写了其中的方法。

# 5 数据类

数据类在 Java 中是没有的。数据类,顾名思义就是用来存储数据的类,所以一般不涉及业务逻辑代码。

Kotlin 提供了便捷的方式来创建数据类,并自动生成一些通用的方法。

举个栗子:

下面定义两个类,一个Student1普通类,一个Student2数据类,看一下他们的区别:

class Student1(val name:String, val age: Int){}
data class Student2(val name:String, val age: Int){}

fun main() {
    val stu11 = Student1("Doubi", 12)
    val stu12 = Student1("Doubi", 12)
    println(stu11)						// Student1@7a79be86
    println(stu11 == stu12)		// false


    val stu21 = Student2("Doubi", 12)
    val stu22 = Student2("Doubi", 12)
    println(stu21)						// Student2(name=Doubi, age=12)
    println(stu21 == stu22)		// true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在上面的代码中,分别使用了普通类和数据类分别创建了两个对象,打印对象并进行了比较。

会发现数据类重写了 toString 方法,所以打印对象的时候,显示的内容和普通类不同。

另外两个数据类对象使用 == 进行比较,会比较两个对象的值是否相等,因为数据类还重写了 equalshashcode 方法。

需要注意:

  • 数据类的主构造函数最少要有一个参数;
  • 主构造函数的参数必须使用 var 或 val 声明;
  • 数据类不能使用 open、abstract、sealed和 inner 修饰。

# copy 函数

数据类还提供了一个 copy 函数,方便复制一个对象,使他们的属性值一样。

举个栗子:

data class Student2(var name:String, val age: Int, val score: Int){
    constructor(name: String):this(name,10, 90) {
        this.name = name
    }
}

fun main() {
    val stu1 = Student2("Doubi", 12, 98)
    val stu2 = stu1.copy(name = "Niubi")       // 调用的是主构造函数,如果想修改指定的属性在这里修改即可
    println(stu1)   // Student2(name=Doubi, age=12, score=98)
    println(stu2)   // Student2(name=Niubi, age=12, score=98)
}
1
2
3
4
5
6
7
8
9
10
11
12

调用 copy 函数,可以指定想要修改的属性的值。需要注意,copy函数不是调用的次要构造函数,所以在次要构造函数中设置属性值不会影响 copy 函数。

# 解构声明

在学习 List 的时候,就介绍过解构语法,使用解构语法,可以使用 List 同时为多个变量赋值。

其实解构声明的实现是通过 component1、component2等若干组件函数实现的,让每个函数负责返回一个属性数据。

我们用普通类来举个栗子:

class Student(var name:String, val age: Int, val score: Int){
    operator fun component1() = name        // 定义解构函数,返回属性值
    operator fun component2() = age
    operator fun component3() = score
}

fun main() {
    val stu = Student("Doubi", 12, 98)
    val (name, age, score) = stu        	// 解构赋值
    println("${name}, ${age}, ${score}")	// Doubi, 12, 98
}
1
2
3
4
5
6
7
8
9
10
11

main() 函数中,创建了一个 Student 类的实例 stu,然后使用解构声明方式将其属性分别赋值给 nameagescore 变量。最后,通过 println() 函数打印了这三个属性的值。

而数据类默认就为所有定义在主构造函数中的属性添加了对应的解构组件函数,所以默认就具有了结构声明。

举个栗子:

data class Student(var name:String, val age: Int, val score: Int){
  // 不需要解构声明,默认就有
}

fun main() {
    val stu = Student("Doubi", 12, 98)
    val (name, age, score) = stu        // 解构赋值
    println("${name}, ${age}, ${score}")
}
1
2
3
4
5
6
7
8
9

数据类默认就有解构声明,所以可以直接进行解构赋值。

# 6 嵌套类

嵌套类 (nested class) 是定义在另一个类内部的类,但它不持有外部类的引用。嵌套类与外部类之间并没有直接的关联,因此它可以直接实例化而无需外部类的对象。和内部类是不同的。

举个栗子:

class OuterClass {
    val name:String = ""

    fun outerFun() {
        println("外部类方法")
    }

    class NestedClass {
        fun nestedFun() {
            println("内部类方法")
            //outerFun()                 // 报错,嵌套类无法直接访问外部类的成员,因为它没有对外部类的引用。
            // println(${name})        // 报错,无法访问name
        }
    }
}

fun main() {
    val nested = OuterClass.NestedClass()
    nested.nestedFun()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

**注意,嵌套类无法直接访问外部类的成员,因为内部类不持有外部类的引用。**嵌套类与外部类之间并没有直接的关联,因此它可以直接实例化而无需外部类的对象。

什么时候会用到嵌套类呢?

如果一个类只对另一个类有用,那么将其嵌入到该类中并使这两个类保持在一起是合乎逻辑的,可以使用嵌套类。

# 7 内部类

内部类 (inner class) 是定义在另一个类内部的类,并且默认持有外部类的引用。它需要依附于外部类的实例而存在,因此每个内部类实例都会关联到创建它的外部类实例。

内部类使用 inner class 来定义。

举个栗子:

class OuterClass {
    val name:String = ""

    fun outerFun() {
        println("外部类方法")
    }

    inner class InnerClass {
        fun innerFun() {
            outerFun()
            println(name)           // 可以直接访问外部成员
        }
    }
}

fun main() {
    val outer = OuterClass()
    val inner = outer.InnerClass()
    inner.innerFun()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

main() 函数中,首先创建了外部类的实例 outer,然后使用该实例创建了内部类 Inner 的实例 inner。内部类实例可以访问外部类的属性和方法。

内部类与外部类对象相关联,每个内部类对象都与创建它的外部类对象相关。这意味着内部类对象不能独立存在,它们总是与外部类对象相关联。

所以可以看出内部类和嵌套类区别还是挺大的:

  1. 嵌套类中,内部类和外部类是独立的类,嵌套类因为不持有外部类对象的引用,所以内部类无法访问外部类的属性和方法,因此也可以直接实例化而无需外部类的实例;
  2. 内部类中,因为内部类持有外部类的引用,所以内部类可以访问外部类的属性和方法,同时内部类不能单独的实例化。

# 8 运算符重载

什么是运算符重载?

正常情况下,我们肯定不能将两个对象进行相加,但是使用运算符重载,我们可以重写 + 运算符,让 + 可以实现两个对象。

举个栗子:

我们定义一个坐标类,里面有 x、y 两个属性,现在实现功能,当使用 + 将两个对象相加时,分别将两个对象的 x、y 分别相加,得到一个新对象。

class Point(var x:Int, var y: Int){
    operator fun plus(o2: Point) = Point(x + o2.x, y + o2.y)		// 需要使用 operator 关键字重写plus方法。
}

fun main() {
    val point1 = Point(10, 20)
    val point2 = Point(10, 20)

    val point3 = point1 + point2
    println(point3.x)       // 20
    println(point3.y)       // 40
}
1
2
3
4
5
6
7
8
9
10
11
12

如果要实现两个对象的相加,需要使用 operator 关键字重写类的 plus 方法。

还有一些其他的运算符,也是使用同样的方式进行运算符重载,不知道怎么操作的,可以百度一下,这里不细讲了。

操作符 函数名 作用
+ plus 把一个对象添加到另一个对象里
+= plusAssign 把一个对象添加到另一个对象里,然后将结果赋值给第一个对象
== equals 如果两个对象相等,则返回true,否则返回false
> compareTo 如果左边的对象大于右边的对象,则返回true,否则返回false
[] get 返回集合中指定位置的元素
.. rangeTo 创建一个range对象
in contains 如果对象包含在集合里,则返回true

# 9 枚举类

枚举类就是用来定义一组常量的特殊类。一般各个语言都会有。

Kotlin 中使用 enum class 来定义枚举类

举个栗子:

enum class Season {     // 定义枚举类
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER
}

fun main() {
    println(Season.SUMMER)          // SUMMER
    println(Season.SUMMER.ordinal)  // 1 枚举的索引顺序
    println(Season.SUMMER.name)     // SUMMER 枚举的名称
    println(Season.SUMMER is Season)  // true
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在上面定义了一个季节的枚举类,包含了春夏秋冬4个常量。通过 Season.SUMMER is Season 可以看到,其中的枚举值是枚举类的实例对象。

枚举类也是类,我们也是可以给枚举类添加构造函数的,那么各个枚举值也就有了属性,一般我们可是使用这种方式来定义错误码。

举个栗子:

enum class ResultCode(val code: Int, val message: String) {     // 定义枚举类
    SUCCESS(0, "成功"),
    SERVER_ERROR(1001, "服务器错误"),
    PARAMETER_ERROR(1002, "参数错误"),
    REQUEST_EXPIRE(1003, "请求过期"),
}

fun main() {
    println(ResultCode.SUCCESS.code)          // 0
    println(ResultCode.SUCCESS.message)       // 成功
    println(ResultCode.SERVER_ERROR.code)       // 1001
    println(ResultCode.SERVER_ERROR.message)     // 服务器错误
}
1
2
3
4
5
6
7
8
9
10
11
12
13

我们可以使用这种方式来定义错误码,包含错误码的 code 和 信息,这样就很清晰了。

枚举类可以包含方法:

enum class Season {

    SPRING {
        override fun getSeasonInfo() {
            println("春天到了,又到了动物们交配的季节")
        }
           },
    SUMMER{
        override fun getSeasonInfo() {
            println("夏天也不错")
        }
    },
    AUTUMN,
    WINTER;

    open fun getSeasonInfo() {	// 枚举类可以定义方法
        println("季节信息")
    }
}

fun main() {
    Season.SPRING.getSeasonInfo()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在上面的代码中可以看到,在枚举类中可以定义方法,而且每个枚举值可以重写枚举类的函数。

注意:枚举值要写在函数前面。

# 10 密封类

密封类和枚举类很相似,它是一种特殊的类,用于限制类的继承结构。密封类允许你定义一组相关的类,并且只允许这些类作为其子类存在。密封类通常用于替代枚举类,在枚举类中,每个实例都是通过枚举类而来的,所以结构是一样的,而在密封类中,每个实例可以不同,只是他们都继承自同一个父类。

密封类的申明使用 sealed 关键字。

举个栗子:

// 定义密封类
sealed class MessageCode {
    object SUCCESS : MessageCode()                      // 定义不同的子类
    class SERVER_ERROR(val code: Int) : MessageCode()   // 每个子类继承相同的父类MessageCode
    class PARAMETER_EMPTY : MessageCode {               // 每个子类可以有自己的处理逻辑
        var names = mutableListOf<String>()

        constructor(vararg names: String):super() {
            this.names.addAll(names)
        }

        fun getInfo() {
            println("为空的参数:${names}")
        }
    }
}

fun showMessage(messageCode: MessageCode) {
    when(messageCode) {                             // 通过判断是不同的子类实例,还做不同的处理
        is MessageCode.SUCCESS -> println("成功")
        is MessageCode.SERVER_ERROR -> println("服务器出错, 错误码:${messageCode.code}")
        is MessageCode.PARAMETER_EMPTY -> messageCode.getInfo()
    }
}

fun main() {
    val messageCode1: MessageCode = MessageCode.SUCCESS
    val messageCode2: MessageCode = MessageCode.SERVER_ERROR(1001)
    val messageCode3: MessageCode = MessageCode.PARAMETER_EMPTY("username", "password")

    showMessage(messageCode1)   // 成功
    showMessage(messageCode2)   // 服务器出错, 错误码:1001
    showMessage(messageCode3)   // 为空的参数:[username, password]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

在上面的代码中,先定义了密封类,密封类中有多个枚举的子类,每个子类都继承密封类父类,每个子类可以有自己的逻辑。

showMessage 函数中,结合 when,通过 is 进行判断,和枚举类似,可以针对不同的值进行不同的处理。