Kotlin 学习之面向对象编程

568 阅读6分钟

Kotlin 和 Java 一样也是面向对象的。不同于面向过程的语言(比如 C 语言),面向对象的语言是可以创建类的。类是对事物的一种封装。类可以拥有自己的字段和函数,字段表示该类所拥有的属性,函数则表示该类有哪些行为。
通过这种类的封装,我们就可以在适当的时候创建该类的对象,然后调用对象中的字段和函数来满足实际编程的需求,这就是面向对象编程的基本思想。除了封装,还有继承、多态等。

类与对象

定义一个类:

class Person {
    var name = ""
    var age = 0
    
    fun eat() {
        println(name + " is eating. He is " + age + " years old.")
    }
}

实例化这个类,即创建一个 Person 对象:

val p = Person()

Kotlin 实例化一个类的方式和 Java 基本类似,只是去掉了 new 关键字而已。

继承与构造函数

继承

比如我们要定义一个 Student 类,让它继承自 Person 类。该如何做呢?

class Student {
    var number = ""
    var grade = 0
}

一、使 Person 类可以被继承
Effective Java 这本书提到,如果一个类不是专门位继承而设计的,那么就应该将它加上 final 声明,禁止它可以被继承。
Kotlin 在设计的时候遵循了这条编程规范,默认所有的非抽象类都是不可以被继承的。之所以强调非抽象类,是因为抽象类本身是无法创建实例的,一定要由子类去继承它才能创建实例。
Kotlin 使用了 open 关键字表明被修饰的类可以被继承。

open class Person {
    ...
}

二、让 Student 类继承 Person 类
在 Java 中继承的关键字是 extends,而在 Kotlin 中变成了一个冒号:

class Student : Person() {
    var number = ""
    var grade = 0
}

继承的写法如果只是替换一下关键字倒也很简单,但是为什么 Person 类的后面要加上一对括号呢?这就涉及到 Kotlin 的主构造函数、次构造函数了。

构造函数

主构造函数
主构造函数是最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然也可以显示地给它指明参数。主构造函数地特点是没有函数体,直接定义在类名地后面即可。如下:

class Student(val number: String, val grade: Int) : Person() {
}

这样进行实例化时,必须传入构造函数要求地参数:

var student = Student("9527", 3)

由于构造函数中的参数时在创建实例的时候传入的,所以我们可以将参数全部声明为 val。
如果我们想在主构造函数中编写一些逻辑的话,可以使用 Kotlin 提供的 init 结构体,所有的主构造函数中的逻辑都可以写在里面:

class Student(val number: String, val grade: Int) : Person() {
    init {
        println("number is " + number)
        println("grade is " + grade)
    }
}

Person 类后面的那对空括号怎么理解?
Java 继承特性有一个规定,子类中的构造函数必须调用父类中的构造函数,这个规定 Kotlin 也需要遵守。那对括号就是 Kotlin 为了解决这个问题而设计的。
在这里,Person 类后面的括号表示 Student 类的主构造函数在初始化的时候会调用 Person 类的无参构造函数,即使在无参数的情况下,这对括号也不能省略。
而如果我们将 Person 类 改造一下:

open class Person(val name: String, val age: Int) {
    ...
}

那么 Student 类需要做如下改造:
1、给 Person 类的构造函数传入 name 和 age 字段
2、在 Student 类的构造函数也加上 name 和 age 字段

class Student(val number: String, val grade: Int, name: String, age: Int) : Person(name, age) {
    ...
}

注意:在 Student 类的主构造函数中增加 name 和 age 这两个字段时,不能再将它们声明成 val,因为在主构造函数中声明成 val 或者 var 的参数将自动成为该类的字段,这就会导致和父类中同名的 name 和 age 字段造成冲突。

次构造函数

任何一个类只能有一个主构造函数,但是没有可以有多个次构造函数。次构造函数有函数体。
Kotlin 规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。举例:

class Student(val number: String, val grade: Int, name: String, age: Int) : Person(name, age) {
    constructor(name: String, age: Int) : this("", 0, name, age) {
        
    }
    constructor() : this("", 0) {
        
    }
}

次构造函数是通过 constructor 关键字来定义的,上面定义了两个次构造函数:第一个次构造函数接收 name 和 age 参数,然后又通过 this 关键字调用了主构造函数;第二个次构造函数不接收任何参数,它通过 this 关键字调用了第一个次构造函数。
那么现在我们就拥有了3种方式来对 Student 类进行实例化。:

val student1 = Student()
val student2 = Student("Jack", 19)
val student3 = Student("9527", 5, "Jack", 19)

Kotlin 中允许类中只有次构造函数,没有主构造函数,如下:

open class Person(val name: String, val age: Int) {
    ...
}
class Student : Person {
    constructor(name: String, age: Int) : super(name, age) {

    }
}

注意到这里的代码变化,首先 Student 类的后面没有显示的定义主构造函数,同时又因为定义了次构造函数,所以现在 Student 类是没有主构造函数的。那么既然没有主构造函数,继承 Person 类的时候也不需要再加上括号了。
另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将 this 关键字换成了 super 关键字。

接口

接口时用于实现多态编程的重要组成部分。Java 是单继承结构的语言,一个类最多继承一个父类,但是可以实现多个接口,Kotlin 也是如此。
Kotlin 也是使用 interface 关键字定义接口,代码如下:

interface Study {
    fun readBooks()
    fun doHomework()
}

接下来可以让 Student 类去实现 Study 接口:

class Student(val number: String, val grade: Int, name: String, age: Int) : Person(name, age), Study {
    override fun readBooks() {
        println(name + " is reading")
    }

    override fun doHomework() {
        println(name + " is doing homework")
    }
}

Kotlin 统一使用冒号来表示继承和实现,使用 override 关键字来重写父类或者实现接口中的函数。
Kotlin 允许对接口中定义的函数进行默认实现。其实 Java 在 JDK 1.8 之后也开始支持这个功能了。

函数的可见性修饰符

修饰符JavaKotlin
public所有类可见所有类可见(默认)
private当前类可见当前类可见
protected当前类、子类、同一包路径下的类可见当前类、子类可见
default同一包路径下的类可见(默认)
internal同一模块中的类可见