# Kotlin教程 - 8 面向对象

面向对象,首先涉及到的两个概念就是:对象

什么是类?

类就是对现实事物的抽象设计。

例如设计学生的类,可能包括属性:学号,姓名、年龄、性别等。

设计狗的类,可能包括属性:名字、年龄、品种。

类表示的是对一个事物的抽象,不是具体的某个事物。

什么是对象?

对象就是一个具体的实例,这个实例是从类派生出来的。

我们将类的属性赋值,就产生了一个个实例,也就是对象。

例如通过学生的类,我们赋值:学号=001,姓名=张三,年龄=16,性别=男,班级=三年二班,产生了就是一个名字叫张三的具体的学生,这样我们通过给类可以派生出一个个不同属性值的对象,李四、王五、赵六...。

# 8.1 类和对象

# 1 类的定义

类中可以定义属性和行为,属性也就是变量,表示这个类有哪些数据信息,行为也叫方法,表示这个类能干什么。

例如,对于学生类而言,学号、姓名、年级就是属性,学习这个行为,可以定义为方法。

那么我们可以定义以下学生类:

定义类使用class关键字,后面跟类名。

class Student {
    var sid: String = "001"				// 定义属性sid表示学号
    var name: String = "Doubi"		// 定时属性name表示姓名
    var age: Int = 12

    fun study() {
        println("我是$name, 我在学习")		// 可以在方法中访问属性
    }
}
1
2
3
4
5
6
7
8
9

上面的类定义了三个成员变量(sid、name、age),并赋值了。还定义了一个成员方法 study,定义成员方法和之前定义函数是一样的,可以在方法中访问属性。

方法在不同的语言中,有的叫函数,后面不区分,方法也叫函数。类中的属性也可能被叫做变量。

# 2 类的使用

上面我们定义好类了,现在可以使用类来创建对象了。然后通过对象访问属性和调用方法。

举个栗子:

class Student {
    var sid: String = "001"
    var name: String = "Doubi"
    var age: Int = 12

    fun study() {
        println("我是$name, 我在学习")    // 可以在方法中访问属性
    }
}

fun main() {
    val stu1 = Student()   // 创建一个对象
    println(stu1.name)     // 访问属性
    println(stu1.age)
    stu1.study()            // 调用方法

    val stu2 = Student()    // 创建第二个对象
    stu2.name = "ShaBi"     // 修改姓名
    stu2.age = 13
    stu2.study()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

通过 类名() 可以创建一个对象。创建对象后,我们可以通过 对象.属性 来访问变量,也可以通过 对象.属性=值 来给属性赋值。

使用 对象.方法() 可以调用方法。

执行结果:

Doubi
12
我是Doubi, 我12岁了, 我在学习
我是ShaBi, 我13岁了, 我在学习
1
2
3
4

面向对象编程就是先设计类,然后通过类创建对象,由对象做具体的工作。

面向过程编程关心的是实现功能的步骤,而面向对象编程更关心由谁来实现功能。

# 3 主构造方法

我们上面在创建类的时候,是给属性直接赋值的String name = "Doubi";,所有的对象的 name 属性都是 "Doubi",这显然是不合理的。但是在实际的使用中,一般是使用不同的属性值创建不同的对象,而且可以在创建对象的时候对属性进行初始化,这里就需要用到构造函数。

构造函数会在创建对象的时候执行,通过传递的属性值,给属性进行初始化。

Kotlin中有两种主要类型的构造方法:主构造方法(Primary Constructor)和次要构造方法(Secondary Constructor)。

先介绍主构造方法,举个栗子:

主构造方法通常与类的定义一起声明,位于类名后面。

/**
 * 定义类
 */
class Student(val name: String, var age: Int) {				// 这里使用val和var定义了参数
    fun study() {
        println("我是$name,我${age}岁了,我在学习")
    }
}

/**
 * 测试
 */
fun main() {
    val stu1 = Student("Doubi", 12)   // 创建一个对象
    println(stu1.name)      // 访问属性
    println(stu1.age)
    stu1.study()            // 调用方法

    val stu2 = Student("Shabi", 13)    // 创建第二个对象
    // stu2.name = "NiuBi"     // 无法修改姓名,因为是只读的
    stu2.age = 15     // 可以修改姓名
    stu2.study()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

执行结果:

Doubi
12
我是Doubi,我12岁了,我在学习
我是NiuBi,我13岁了,我在学习
1
2
3
4

在上面的代码中,在类名后面定义了主构造方法的参数,构造方法声明了两个属性:name(只读属性,因为它是使用关键字val声明的)和 age(读写属性,因为它是用关键字var声明的),那么在创建对象的时候就需要根据参数列表中的参数传参来创建对象。

在上面使用 val和var 来定义参数,这种方式声明的主构造函数的参数同时也是类的属性,所以参数 name 和 age 直接变成了 Student 类中的属性,这种方式很简洁。

甚至,如果类中如果没有方法,可以省略类体。

举个栗子:

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

fun main() {
    val stu = Student("Doubi", 12)
    println(stu.name)			// Doubi
}
1
2
3
4
5
6

像上面的 Student 没有类体,这种类一般用来封装数据,后面再讲。

但是有些时候,不想通过构造方法的参数来确定类的属性,可以去掉 var ,举个栗子:

/**
 * 定义类
 */
class Student(name: String, age: Int) {			// 这里不使用var
    var name:String = name
    var age:Int = age + 1
    var isAdult:Boolean = age >= 18

    fun study() {
        println("我是$name,我${age}岁了,我在学习")
    }
}

/**
 * 测试
 */
fun main() {
    val stu1 = Student("Doubi", 12)   // 创建一个对象
    println(stu1.name)      // 访问属性
    println(stu1.age)
    stu1.study()            // 调用方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

不使用 var 声明的主构造函数的参数是普通的局部变量,需要重新定义属性,将参数赋值给属性。

这样我们可以对参数进行一些额外的处理再赋值给属性,就像上面,我们将 age 参数的值 +1再赋值给属性,通过 age 的值处理后赋给isAdult。

# 4 init初始化代码块

在主构造函数中是不能包含任何的代码的,所以如果初始化的时候有比较复杂的逻辑,可以使用init 初始化代码块。

init 初始化代码块是一个特殊的代码块,它会在主构造方法之后执行,用于执行与属性初始化相关的逻辑。

举个栗子:

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

    init {
        // 初始化代码块
        // 在这里可以执行额外的初始化操作,可以访问主构造方法中的参数
        this.name = name
        this.age = if (age >= 0) age else 0 // 对年龄进行验证并初始化
    }

    fun study() {
        println("我是$name,我${age}岁了,我在学习")
    }
}

/**
 * 测试
 */
fun main() {
    val stu1 = Student("Doubi", 2)   // 创建一个对象
    println(stu1.name)      // 访问属性
    println(stu1.age)
    stu1.study()            // 调用方法
}
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

init 初始化代码块中,你可以访问主构造方法中的参数和属性,并执行各种初始化操作。通常,init 初始化代码块用于在属性初始化之前执行某些逻辑,例如验证属性的有效性、计算属性的初始值等。

在上面用到了 this 关键字,因为参数的名字是 name,属性的名字也是 name,那么在同一段代码中,没办法区分是属性还是参数,此时的 this 表示调用当前对象的属性。当然你也可以将属性的名称和参数的名称不同,就不存在这个问题了。

# 5 this的作用

在上面的代码中用到了 this,因为上面我们写构造函数的时候,形参和类的属性名相同(当然也可以不同),导致在构造函数中,使用 sid/name/age 无法知道访问的是类的属性还是形参,使用 this,就表示访问的是属性 。

其实 this 表示的是调用当前方法的对象。如何理解?

举个栗子:

下面的代码,我们定义了学生类,创建了两个学生的对象。

/**
 * 定义类
 */
class Student(val name: String, var age: Int) {				// 这里使用val和var定义了参数
    fun study() {
        println("我是${this.name},我${this.age}岁了,我在学习")
    }
}

/**
 * 测试
 */
fun main() {
    val stu1 = Student("Doubi", 12)
    stu1.study()

    val stu2 = Student("Shabi", 13)
    stu2.study()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

执行结果:

我是Doubi, 我12岁了, 我在学习
我是Shabi, 我13岁了, 我在学习
1
2

当我们使用stu1调用study()方法的时候,this 就是指张三这个对象,那么this.name 的值就是张三;当我们使用stu2调用study()方法的时候,this就是指李四这个对象,那么 this.name 的值就是李四。

# 6 主构造函数中的默认值

构造函数还可以提供默认值,这样如果不传递参数,参数将使用默认值。

举个栗子:

/**
 * 定义类
 */
class Student(val name: String = "UNKNOW", var age: Int = 0) {				// 这里使用val和var定义了参数
    fun study() {
        println("我是${this.name},我${this.age}岁了,我在学习")
    }
}

/**
 * 测试
 */
fun main() {
    val stu1 = Student("Doubi", 12)
    stu1.study()

    val stu2 = Student("Niubi")
    stu2.study()

    val stu3 = Student()
    stu3.study()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

构造函数提供个默认值后,在创建对象的时候,有默认值的参数就可以省略传参,使用默认值。

执行结果:

我是Doubi,我12岁了,我在学习
我是Niubi,我0岁了,我在学习
我是UNKNOW,我0岁了,我在学习
1
2
3

# 7 次要构造方法

在Kotlin中,一个类还可以包含0个或多个次构造方法。它们是使用 constructor 关键字创建的。

当需要以不同的参数初始化类的多个构造方法的类时,就可以使用次要构造方法了。

举个栗子:

/**
 * 定义类
 */
class Student {
    var name:String = ""
    var age:Int = 0

    /**
     * 次要构造方法,没有参数
     */
    constructor() {
        // 初始化代码
    }

    /**
     * 次要构造方法2,接收一个参数
     */
    constructor(name: String) {
        // 初始化代码
        this.name = name
    }

    /**
     * 次要构造方法3,接收两个参数
     */
    constructor(name: String, age: Int) {
        // 初始化代码
        this.name = name
        this.age = age
    }
}

/**
 * 测试
 */
fun main() {
    val stu1 = Student()
    val stu2 = Student("Doubi")
    val stu3 = Student("Doubi", 12)
}
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
35
36
37
38
39
40

在上面使用了 constructor 关键字定义了3个次要构造方法,在创建对象的时候可以通过使用不同的参数来调用不同的构造方法来创建对象。

# 8 无参构造方法

如果一个类写没有任何的构造函数,那么类是有一个隐式的无参主构造函数,如果写了其他的构造函数,则隐式的构造函数失效。

举个栗子:

/**
 * 定义类
 */
class Student {

}

/**
 * 测试
 */
fun main() {
    val stu = Student()
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在上面没有写任何的构造函数,所以类是由一个隐式的无参构造函数的,所以可以通过 Student() 来创建对象。

如果写了显示的构造函数:

/**
 * 定义类
 */
class Student(val name:String) {

}

/**
 * 测试
 */
fun main() {
  	// val stu1 = Student()		// 报错,没有无参构造函数
    val stu2 = Student("Doubi")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面显式的定义了构造函数,所以隐式的无参构造函数就没有了,所以无法通过 Student() 来创建对象。当然如果为参数设置了默认值,是可以调用的。

同样如果类中创建了次要构造函数,编译器也不会创建隐式的构造函数了。

举个栗子:

class Student {
    constructor(name:String) {
        
    }
}

/**
 * 测试
 */
fun main() {
    // val stu1 = Student()		// 报错,没有无参构造函数
    val stu2 = Student("Doubi")
}
1
2
3
4
5
6
7
8
9
10
11
12
13

上面定义了次要构造函数,那么类中也没有了隐式的无参的主构造函数,如果需要可以显式定义一个无参的主构造函数。

我们可以在类名后添加括号,不添加参数,就是无参主构造函数了,代码如下:

class Student() {
    constructor(name:String) {			// 代码报错

    }
}

/**
 * 测试
 */
fun main() {
    val stu1 = Student()		
    val stu2 = Student("Doubi")
}
1
2
3
4
5
6
7
8
9
10
11
12
13

但是上面的次要构造方法的代码会报错,这是为什么呢?

这里需要注意次要构造方法的几个问题。

# 9 构造函数的注意点

1、当在类中定义主构造函数后,也就是在类名后有了括号,那么类中的次构造函数都必须使用this关键字直接间接的调用主构造函数。

举个栗子:

class Student() {
    /**
     * 次要构造函数1
     */
    constructor(name:String):this() {

    }
    /**
     * 次要构造函数2
     */
    constructor(name:String, age:Int):this(name) {  // 通过调用次要构造函数1来调用主构造函数

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面的代码中,因为定义了主构造函数,那么那么次要构造函数就必须使用this关键字调用主构造函数,当然也可以是间接调用(通过其他次要构造函数调用)。

再举个例子:

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

    }
    /**
     * 次要构造函数2
     */
    constructor(age:Int):this("", 12) {  

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在上面的代码中,同样次要构造函数也必须要使用this 关键字调用主构造函数。

上面都是显式定义了主构造函数,如果没有显示定义主构造函数,直接定义次要构造函数,那么就没有隐式的主构造函数,就不会有问题了。

2、不能使用次要构造函数的参数来定义类的属性。

在前面,我们可以通过主构造函数的参数列表来定义类的属性。

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

/**
 * 测试
 */
fun main() {
    val stu = Student("Doubi", 12)
    println(stu.name);
}
1
2
3
4
5
6
7
8
9
10

在上面使用 valvar 关键字,通过主构造函数来创建了类的属性,这时类中是包含了 nameage 属性。

但是次要构造函数的参数定义是不能使用 valvar 关键字的。

class Student {
    constructor(val name:String, var age:Int) { // 代码报错,不能使用val和var
    }    
}
1
2
3
4

# 10 初始化顺序

在上面讲解的内容中,我们可以在很多地方对属性进行初始化,那么它们初始化的执行顺序是怎么样的呢?

  1. 首先执行主构造函数的初始化,也就是先给主构造函数参数列表中使用了 varval 声明的属性进行赋值;
  2. 然后给类中定义的属性赋值;
  3. 执行 init 初始化代码块,为属性赋值;
  4. 调用次构造函数里的代码,为属性赋值;
class Student(var name: String, age: Int) {     // 1
    var age:Int = age       // 2

    init {
        this.age = if (age >= 0) age else 0     // 3
    }
    
    constructor(name: String) : this(name, 10) {
        this.name = name    // 4
    }
}
1
2
3
4
5
6
7
8
9
10
11

将字节码反编译成 Java 代码,会看的很清楚了,这里就不细讲了。下面再看初始化问题。

举个栗子:

class Student(var name: String, age: Int) {
    init {
        this.age = if (age >= 0) age else 0     // 这句代码报错
    }
    var age:Int
}
1
2
3
4
5
6

上面的初始化块的代码报错,提示 age 没有被声明,所以我们需要将属性的定义写到初始化代码块的前面。

再举个例子:

class Student(name: String) {
    val nickname: String = initNickname()
    val name: String = name;

    fun initNickname(): String {
        return name
    }

}

fun main() {
    val stu = Student("Doubi")
    println(stu.nickname)               // null
    println(stu.nickname.length)        // NullPointerException
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面的代码看上去也没什么问题,但是运行的时候 nicknamenull,导致报空指针异常,这是为什么呢?

这其实和属性的初始化的顺序有关,代码先对 nickname 进行初始化,调用了 initNickname() 方法获取初始化的值,而初始化的值是取自 name,此时 name 还没有初始化,还是 null,这就导致 nickname 的值为 null

这在 Java 中是不会出现这种问题的,所以在 Kotlin 中要注意属性的书写顺序,以免初始化出问题。

# 11 延迟初始化

类中的非空属性,必须在创建对象的时候进行初始化,否则会报错。

举个栗子:

class Student {
    var name:String
    var age:Int
}
1
2
3
4

上面的代码是会报错的,因为定义了非空的属性,但是没有给这些属性进行初始化,我们需要在构造函数init 代码块中对属性进行初始化才可以。

当然可以将属性定义为可空,然后初始化为 null ,代码就不会报错了。

class Student {
    var name:String? = null
    var age:Int? = null
}
1
2
3
4

而如果就想定义非空的属性,并且推迟初始化,那么可以使用 lateinit 关键字来标记一个属性,表示该属性将在稍后的某个时刻被初始化。

class Student {
    lateinit var name:String
    var age:Int = 0
}
1
2
3
4

需要注意:lateinit只能用于对可变(var定义的)、非空、非基本数据类型,因为上面的 age 属性是 Int,属于基本数据类型,所以不能使用 lateinit 修饰。

我们在使用延迟初始化的属性前,必须确保属性确实已经被初始化了,否则会报错。

下面的代码中,name没有被初始化,所以访问的时候报错。

/**
 * 定义类
 */
class Student {
    lateinit var name:String
    var age:Int = 0

    fun printInfo() {
        println("${name}")		// 没有被初始化,报错:lateinit property name has not been initialized
        println("${age}")
    }
}

/**
 * 测试
 */
fun main() {
    val stu = Student()
  	// stu.name = "Doubi"		// 在调用printInfo之前需要初始化name,否则会报错
    stu.printInfo()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

为了不报错,我们可以使用 ::变量名.isInitialized 来判断属性是否已初始化。

举个栗子:

/**
 * 定义类
 */
class Student {
    lateinit var name:String
    var age:Int = 0

    fun printInfo() {
        if (::name.isInitialized) {
            println("${name}")
        }
        println("${age}")
    }
}

/**
 * 测试
 */
fun main() {
    val stu = Student()
    stu.printInfo()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 12 惰性初始化

惰性初始化表示初始化的代码我已经准备好了,只是先不执行,等我用到这个属性的时候才会去执行初始化代码。

举个栗子:

class Config {
    val data:String by lazy { loadConfig() }

    fun loadConfig(): String {
        println("正在加载配置...")
        return "xxxx"
    }
}

fun main() {
    val config = Config()
    println(config.data)    // 这里会用到data,才会去调用loadConfig加载配置
}
1
2
3
4
5
6
7
8
9
10
11
12
13

上面针对 Config 类中的 data 属性使用 lazy 属性进行初始化,但是只会在用到 data 的时候才会调用 loadConfig() 方法对 data 进行初始化。

需要注意,lazy关键字只能用于val属性,而不能用于var属性。对于var属性来说,由于其值是可以被修改的,使用lazy关键字会带来一些问题。

# 8.2 封装

面向对象编程,是一种编程思想,简单的理解就是:先有模板,也就是类,根据模板去创建实例,也就是对象,然后使用对象来完成功能开发。

我们经常说面向过程编程和面向对象编程,面向过程编程关注的是实现功能的步骤,面向对象编程更关注的是谁来实现功能。

面向对象编程有3大特性:

  • 封装
  • 继承
  • 多态

下面依次开始讲起。

在前面我们创建类,在类中定义了属性和方法,通过属性和方法来对现实世界的事物进行抽象的描述。

一个事物有很多的属性和方法,但是并不是所有属性和方法都需要开放出来。例如我们定义了一个手机类,我们可以使用手机打电话、拍照等,但是我们并不关心手机电压,驱动信息,也不关心内存的分配,CPU的调度等,虽然这些都属于手机的属性和行为。

我们可以将用户不关心的属性和方法封装并隐藏起来,只给类内部的方法调用,例如上网会用到4G模块,但是不是由用户来使用4G模块,而是由手机上网的功能开调用4G模块,只开放用户直接使用的信息和功能。

那么回过头来看,什么是封装?

它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。

# 1 私有属性和方法

那么怎么将不想暴露的变量和方法隐藏起来呢,就需要用到私有成员变量和私有成员方法。

定义私有成员的方式:在变量或方法前面添加 private 关键字来修饰。

举个栗子:

class Phone {
    var producer: String = "华为" 		 // 手机品牌
    private var voltage: Int = 12 		// 电压

    fun call() {
        println("打电话")
        println("手机品牌:$producer")
        println("手机电压:$voltage")
    }

    // 定义一个私有方法
    private getRunVoltage () {
        println("当前电压:$voltage")
    }
}

fun main() {
    val phone = Phone()
    phone.call()

    phone.producer = "小米"
		// phone.voltage = 24 	 // 编译错误,私有属性无法访问
    phone.call()
		// phone.getRunVoltage() // 编译错误,私有方法无法访问
}
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

上面定义了私有属性 voltage 和私有方法 getRunVoltage,私有属性和私有方法只能在类内部的方法中调用,不能通过对象来调用。

通过构造函数也可以来定义私有属性:

class Phone(private var producer: String, private var voltage: Int) {
}
1
2

# 2 getter和setter

我们前面说到,封装是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。

在面向对象编程思想中,我们一般会将所有的属性都设置为私有的,然后为每个属性提供两个对应的方法,分别用来获取和设置对应的属性,这两个方法称为 gettersetter 方法,在 Java 中就是这么做的。

隐式getter/setter

但其实在 Kotlin 中,在类中创建了属性后,会自动添加 gettersetter 方法。

举个栗子:

class Student(var name: String, var age: Int) {				// 这里使用val和var定义了参数
}

fun main() {
    val stu = Student("Doubi", 12)   // 创建一个对象
    println(stu.name)       // 这里其实是调用get方法
    println(stu.age)
}
1
2
3
4
5
6
7
8

通过将字节码反编译成 Java 代码,可以发现:

显式getter/setter

当然我们也可以显式的定义 gettersetter,显式的 gettersetter,可以对属性的设置和获取进行一些自定义的处理。

举个栗子:

class Student {
    var name: String = ""
        get() = field.uppercase()   // 将name转换为大写
        set(value) {
            field = value.trim()    // 去掉name的前后空格
        }

    var age: Int = 0
        get() = field
        set(value) {
            field = value
        }
}

fun main() {
    val stu = Student()   // 创建一个对象
    stu.name = " doubi "
    stu.age = 10
    println(stu.name)       // DOUBI
    println(stu.age)        // 10
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在上面的代码中,针对 name 和 age 显式的定义了 getter 和 setter。其中的 field 表示的就是对应的属性,当然在 getter 和 setter不是必须操作 field,上面 name 的 getter 方法完全可以写成 get() = "DaShabi"

getter 和 setter 要紧跟属性,但是不一定要缩进。

如果不想提供 getter 和 setter,那么使用 private 修饰属性,让属性变为私有属性即可。

如果只想提供 getter,那么可以将 setter 使用 private 修饰。

举个栗子:

var name: String = ""
    get() = field
    private set(value) {				// 将 setter 设置为私有
        field = value.trim()    
    }
1
2
3
4
5

这里还有一个问题需要注意,先看一下下面的代码:

class Student {
    var name: String = ""

    fun getName(): String {
        return name
    }
}
1
2
3
4
5
6
7

上面的代码看上去没有什么问题,但是其实上面的代码是会报错的。为什么呢?

因为编译器为属性提供了默认的 getter 和 setter,所以其实是已经有一个函数叫 getName ,这个时候再定义一个 getName 的函数就冲突了。

# 8.3 包

前面在学习 Kotlin 的时候,基本所有的代码都是写在一个 kt 文件中,最多也只是简单的使用了 import 关键字,引入了其他的 kt 文件。在实际的开发中,代码量是很大的,会有很多的 kt 文件,通过使用包管理,我们可以更好将相关的功能或类组织在一起,以便更好地维护和复用代码。

所以包的作用主要是:

  1. 把相关功能、类、接口组织到一起;
  2. 相同的包中不能存在同名的类,但是在不同的包中可以有相同类名的类,避免名字冲突;
  3. 通过包可以先定访问权限,拥有包权限才能访问包中类。

下面演示一下包的使用。

# 1 创建包

要创建一个包,只需在 Kotlin 文件的顶部使用 package 关键字并指定包的名称即可。

但和 Java 不同 ,包名和文件夹名可以不一致!

举个栗子:

创建 Student.kt 文件,并编写代码。

package com.doubi.domain		// 定义包

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

fun hello() {
    println("Hello World!")
}

fun hello(name: String) {
    println("Hello ${name}")
}

fun goodBye() {
    println("Good bye")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在上面的代码中,我们创建了一个 Student 类和三个函数,并定义了包 com.doubi,表示 Student 类和三个函数都是在 com.doubi 包下的。

# 2 导入包

然后在 Main.kt 中引入并使用 Student 类和三个函数。

import com.doubi.domain.Student		// 导入类
import com.doubi.domain.hello			// 导入hello函数,重载的两个函数都将导入
import com.doubi.domain.goodBye

fun main() {
    val stu = Student("Doubi", 12)
    println(stu.name)

    hello()
    hello("Doubi")
    goodBye()
}
1
2
3
4
5
6
7
8
9
10
11
12

在上面的代码中,使用 import 关键字导入了 Student 类和三个函数。在 Student.kt 文件中定义两个 hello 函数,使用一个语句导入即可。

如果多个导入语句麻烦,还可以使用通配符将一个文件中的内容都导入。那么可以这样导入:

import com.doubi.domain.*			// 使用通配符导入

fun main() {
    val stu = Student("Doubi", 12)
    println(stu.name)

    hello()
    hello("Doubi")
    goodBye()
}
1
2
3
4
5
6
7
8
9
10

上面代码的文件结构是这样的:

虽然在 Kotlin 中,包名和文件夹名可以不一致,但是,强烈建议按照惯例将包名与文件夹结构保持一致,这样可以更清晰地组织代码,这种对应关系有助于更好地组织和管理代码,否则会很混乱。

所以上面的 Student 类,建议放在 com.doubi.domain 目录下。

可以在 kotlin 文件夹上右键 New--> Package,输入:com.doubi.domain

# 3 别名导入

如果我们有多个包中存在相同类名的类或函数,那么在同一个文件中导入它们,就会报错。

我们可以在导入类或函数的时候,用 as 关键字为它们起个别名。

举个栗子:

import com.doubi.domain.Student as DStudent		// 为类起个别名
import com.doubi.pojo.Student as PStudent

import com.doubi.domain.hello as dHello		// 为函数起个别名
import com.doubi.pojo.hello as pHello

fun main() {
    val stu1 = DStudent("Doubi", 12)
    val stu2 = PStudent("Niubi", 12)

    dHello()
    pHello()
}
1
2
3
4
5
6
7
8
9
10
11
12
13

名称是自定义的,根据自己需要来。通过别名,可以避免冲突。