在 Kotlin 中使用类和对象

1. 前言

在本 Codelab 中,您将学习如何在 Kotlin 中使用类和对象。

类提供了用于构造对象的蓝图。对象是类的实例,其中包含相应对象的专属数据。对象和类实例可互换使用。

打个比方,假设您要建造一栋房子。“类”就好比建筑师的设计方案(也称为蓝图)。蓝图不是真正的房子,而是关于如何建造房子的说明。房子是根据蓝图建造的实际事物或物体。

就像房屋蓝图规划了多个房间,而每个房间都有自己的设计和用途一样,每个类也都有着各自的设计和用途。若要了解如何设计类,您必须熟悉面向对象的编程 (OOP),通过该框架学习如何将数据、逻辑和行为封装到对象中。

OOP 可帮助您将复杂的实际问题简化为较小的对象。OOP 涵盖以下四个基本概念,您会在本 Codelab 后面的部分中详细了解各个概念:

  • 封装:将相关属性和针对这些属性执行操作的方法封装在类中。以手机为例,它封装了摄像头、显示屏、存储卡以及其他一些硬件和软件组件。您不必担心这些组件的内部连接方式。
  • 抽象:封装的扩展,其目的是尽可能隐藏内部实现逻辑。例如,如果您要使用手机拍照,只需打开相机应用,将手机对准要拍摄的场景,然后点击按钮即可。您不需要了解相机应用的构建方式或手机上相机硬件的实际运作方式。简而言之,相机应用的内部机制以及移动设备相机的拍照方式已经过抽象,可让您专心执行重要的任务。
  • 继承:可让您通过建立父子关系来基于其他类的特性和行为构建类。例如,不同的制造商生产各种运行 Android OS 的移动设备,但每种设备的界面都不同。换言之,制造商会继承 Android OS 的功能,并在这个基础上构建自己的自定义功能。
  • 多态性:Polymorphism(多态性)这个单词是希腊语词根“poly-”(意为许多)和“morphism”(意为形态)的合成词。多态性是指以单一、通用的方式使用不同对象的能力。例如,当您将蓝牙音箱连接到手机后,手机只需要知道目前有设备可通过蓝牙播放音频。虽然可供您选择的蓝牙音箱有很多种,但手机不必知道各个音箱的具体使用方式。

最后,您将了解属性委托,它们提供的可重复使用的代码能让您用简洁的语法来管理属性值。在本 Codelab 中,您将构建智能家居应用的类结构,并在构建过程中学习这些概念。

前提条件

  • 了解如何在 Kotlin 园地中打开、修改和运行代码。
  • 了解 Kotlin 编程基础知识,包括变量、函数以及 println()main() 函数

学习内容

  • OOP 概览。
  • 什么是类?
  • 如何使用构造函数、函数和属性定义类?
  • 如何实例化对象?
  • 什么是继承?
  • IS-A 关系和 HAS-A 关系之间的差异。
  • 如何替换属性和函数?
  • 什么是可见性修饰符?
  • 什么是委托以及如何使用 by 委托?

构建内容

  • 智能家居应用的类结构。
  • 代表智能设备(如智能电视和智能灯)的类。

所需条件

  • 可连接到互联网的计算机和网络浏览器

2. 定义类

定义类时,您需要指定该类的所有对象都应具有的属性和方法。

类定义以 class 关键字开头,后面依次跟名称和一对大括号。左大括号之前的语法部分也称为类标头。在大括号之间,您可以指定类的属性和函数。您很快就会学到属性和函数。类定义的语法如以下示意图所示:

该语法是以类关键字开头,后跟名称和一对左/右大括号。大括号之间包含用于描述蓝图的类主体。

以下是建议遵循的类命名惯例:

  • 可以选择任何想要的类名称,但不要将 Kotlin 关键字用作类名称,例如 fun 关键字。
  • 类名称采用 PascalCase 大小写形式编写,因此每个单词都以大写字母开头,且各个单词之间没有空格。以“SmartDevice”为例,每个单词的第一个字母都大写,且单词之间没有空格。

类由以下三大部分组成:

  • 属性:用于指定类对象属性的变量。
  • 方法:包含类的行为和操作的函数。
  • 构造函数:一种特殊的成员函数,用于在定义类的整个程序中创建类的实例。

这不是您第一次使用类。在之前的 Codelab 中,您已经了解 IntFloatStringDouble 等数据类型。在 Kotlin 中,这些数据类型被定义为类。在定义如以下代码段所示的变量时,您将创建 Int 类的对象(该类使用值 1 进行实例化):

val number: Int = 1

定义 SmartDevice 类:

  1. Kotlin 园地中,将内容替换为空的 main() 函数:
fun main() {
}
  1. main() 函数前面的代码行上,定义一个主体包含 // empty body 注释的 SmartDevice 类:
class SmartDevice {
    // empty body
}

fun main() {
}

3. 创建类的实例

如您之前所学,类是对象的蓝图。Kotlin 运行时使用类(即蓝图)来创建该特定类型的对象。有了 SmartDevice 类,您就有了表示智能设备的蓝图。为了在程序中呈现出一个真实的智能设备,您需要创建一个 SmartDevice 对象实例。实例化语法是以类名称开头,后跟一对圆括号,如下图所示:

1d25bc4f71c31fc9.png

若要使用某个对象,您需要创建该对象,并将其赋给变量,方法与定义变量的方式类似。您可以使用 val 关键字来创建不可变变量,使用 var 关键字来创建可变变量。valvar 关键字后依次跟变量名称、= 赋值运算符和类对象的实例化。语法如下图所示:

f58430542f2081a9.png

SmartDevice 类实例化为对象:

  • main() 函数中,使用 val 关键字创建名为 smartTvDevice 的变量,并将其初始化为 SmartDevice 类的实例:
fun main() {
    val smartTvDevice = SmartDevice()
}

4. 定义类方法

在第 1 单元中,您学习了:

  • 函数的定义会使用 fun 关键字,后跟一对圆括号和一对大括号。大括号包含的代码会提供执行任务时所需的指令。
  • 调用函数时,系统会执行该函数中包含的代码。

类可以执行的操作在类中被定义为函数。例如,假设您拥有智能设备、智能电视或智能灯,这些设备都可通过手机来开启和关闭。在编程过程中,智能设备对应于 SmartDevice 类,并且可以用 turnOn()turnOff() 函数表示开关该设备的操作(分别用于实现开启和关闭行为)。

在类中定义函数的语法与您之前学习的语法相同。唯一的区别在于,该函数是放在类主体中。在类主体中定义函数时,该函数称为成员函数或方法,用于表示类的行为。在本 Codelab 的剩余部分中,出现在类主体内的函数一律称为方法。

SmartDevice 类中定义 turnOn()turnOff() 方法:

  1. SmartDevice 类的主体中,定义主体为空的 turnOn() 方法:
class SmartDevice {
    fun turnOn() {

    }
}
  1. turnOn() 方法的主体中,添加 println() 语句,然后向其传递 "Smart device is turned on." 字符串:
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. turnOn() 方法之后,添加用于输出 "Smart device is turned off." 字符串的 turnOff() 方法:
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

对象调用方法

到目前为止,您定义了一个类来作为智能设备的蓝图、创建了该类的实例,并将该实例赋给了变量。现在,您可以使用 SmartDevice 类的方法来开启和关闭设备。

如需调用类中的方法,做法与您在之前的 Codelab 中从 main() 函数调用其他函数的方式类似。例如,如果您需要从 turnOn() 方法调用 turnOff() 方法,可以编写与以下代码段类似的内容:

class SmartDevice {
    fun turnOn() {
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

若要在类的外部调用类方法,请以类对象开头,后面依次跟 . 运算符、函数名称和一对圆括号。可视情况在圆括号中包含方法所需的实参。语法如下图所示:

fc609c15952551ce.png

对该对象调用 turnOn()turnOff() 方法:

  1. main() 函数中 smartTvDevice 变量后面的代码行上,调用 turnOn() 方法:
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. turnOn() 方法后面的代码行上,调用 turnOff() 方法:
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. 运行代码。

输出如下所示:

Smart device is turned on.
Smart device is turned off.

5. 定义类属性

在第 1 单元中,您学习了变量,了解到变量是单个数据的容器。此外,您还学习了如何使用 val 关键字创建只读变量,以及如何使用 var 关键字创建可变变量。

方法用于定义类可以执行的操作,而属性用于定义类的特性或数据属性。例如,智能设备具有以下属性:

  • 名称:设备的名称。
  • 类别:智能设备的类型,例如娱乐、公共事业或烹饪。
  • 设备状态:设备是处于开启、关闭、在线还是离线状态。当设备连接到互联网时,会被视为处于在线状态;否则,就会被视为处于离线状态。

从根本上来讲,属性是在类主体(而非函数主体)中定义的变量。也就是说,用于定义属性和变量的语法都相同。您可以使用 val 关键字定义不可变属性,使用 var 关键字定义可变属性。

实现上述特性作为 SmartDevice 类的属性:

  1. turnOn() 方法前面的代码行上,定义 name 属性并为其赋予 "Android TV" 字符串:
class SmartDevice {

    val name = "Android TV"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. name 属性后面的代码行上,定义 category 属性并为其赋予 "Entertainment" 字符串,然后定义 deviceStatus 属性并为其赋予 "online" 字符串:
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. smartTvDevice 变量后面的代码行上,调用 println() 函数,然后向其传递 "Device name is: ${smartTvDevice.name}" 字符串:
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. 运行代码。

输出如下所示:

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

属性中的 getter 和 setter 函数

属性的用途比变量更广泛。例如,假设您创建了一个类结构来表示智能电视。您会执行的常见操作之一是调高和调低音量。如需在编程中表示此操作,您可以创建一个名为 speakerVolume 的属性,其中包含电视音箱当前设置的音量,但音量值有范围限制。可设置的音量下限为 0,上限为 100。若要确保 speakerVolume 属性始终不超过 100 或低于 0,您可以编写 setter 函数。在更新属性值时,您需要检查该值是否处于 0 到 100 的范围内。再举一例,假设您必须确保名称始终大写。您可以实现 getter 函数,将 name 属性转换为大写。

在深入了解如何实现这些属性之前,您需要了解用于声明这些属性的完整语法。定义可变属性的完整语法是以变量定义开头,后跟可选的 get()set() 函数。语法如下图所示:

f2cf50a63485599f.png

如果您没有为属性定义 getter 和 setter 函数,Kotlin 编译器会在内部创建这些函数。例如,如果您使用 var 关键字来定义 speakerVolume 属性并为其赋予值 2,编译器会自动生成 getter 和 setter 函数,如以下代码段所示:

var speakerVolume = 2
    get() = field  
    set(value) {
        field = value    
    }

您不会看到这几行代码,因为它们是由编译器在后台添加的。

immutable不可变属性的完整语法有以下两处差异:

  • val 关键字开头。
  • val 类型的变量为只读变量,因此不含 set() 函数。

Kotlin 属性使用后备字段在内存中存储值。从根本上来讲,后备字段是在属性内部定义的类变量。后备字段的作用域限定为相应属性,这意味着您只能通过 get()set() 属性函数访问该字段。

如果想读取 get() 函数中的属性值或更新 set() 函数中的值,您需要使用对应属性的后备字段。该字段是由 Kotlin 编译器自动生成,并通过 field 标识符来引用。

例如,如果您要更新 set() 函数中的属性值,可使用 set() 函数的形参(称为 value 形参),并将其赋给 field 变量,如以下代码段所示:

var speakerVolume = 2
    set(value) {
        field = value    
    }

例如,若要确保赋给 speakerVolume 属性的值介于 0 到 100 之间,您可以实现 setter 函数,如以下代码段所示:

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

您可以在 set() 函数中使用 in 关键字,并在后面加上值的范围,以检查 Int 值是否处于 0 到 100 的范围内。如果该值在预期范围内,系统便会更新 field 值;否则,该属性的值保持不变。

在本 Codelab 的“实现类之间的关系”部分中,您会在类中包含这个属性,因此现在无需在代码中添加 setter 函数。

6. 定义构造函数

构造函数的主要用途是指定类对象的创建方式。换言之,构造函数用于初始化对象,使其可供使用。您在实例化对象时就已执行此操作。在实例化类的对象时,系统会执行构造函数中的代码。您可以定义包含形参或不含形参的构造函数。

默认构造函数

默认构造函数不含形参。定义默认构造函数的做法如以下代码段所示:

class SmartDevice constructor() {
    ...
}

Kotlin 旨在简化代码,因此,如果构造函数中没有任何注解或可见性修饰符(您将在稍后学习这部分内容),您可以移除 constructor 关键字。如果构造函数中没有任何形参,您还可以移除圆括号,如以下代码段所示:

class SmartDevice {
    ...
}

Kotlin 编译器会自动生成默认构造函数。您不会在自己的代码中看到自动生成的默认构造函数,因为编译器会在后台进行添加。

定义形参化构造函数

SmartDevice 类中,namecategory 属性不可变。您需要确保 SmartDevice 类的所有实例都会初始化 namecategory 属性。在当前实现中,namecategory 属性的值都采用硬编码。也就是说,所有智能设备都是以 "Android TV" 字符串命名,并使用 "Entertainment" 字符串进行分类。

若要保持不变性,同时避免使用硬编码值,请使用形参化构造函数进行初始化:

  • SmartDevice 类中,将 namecategory 属性移至构造函数中,且不赋予默认值:
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

现在,该构造函数可接受形参来设置其属性,因此,为此类实例化对象的方式也会随之更改。实例化对象的完整语法如下图所示:

bbe674861ec370b6.png

代码表示形式如下:

SmartDevice("Android TV", "Entertainment")

构造函数的这两个实参都是字符串,因此我们不清楚应该为哪个形参赋值。解决此问题的做法与传递函数实参的方式类似,只需创建包含具名实参的构造函数即可,如以下代码段所示:

SmartDevice(name = "Android TV", category = "Entertainment")

Kotlin 中的构造函数主要有两类:

  • 主要构造函数:一个类只能有一个主要构造函数(在类标头中定义)。主要构造函数可以是默认构造函数,也可以是形参化构造函数。主要构造函数没有主体,表示其中不能包含任何代码。
  • 辅助构造函数:一个类可以有多个辅助构造函数。您可以定义包含形参或不含形参的辅助构造函数。辅助构造函数可以初始化类,具有包含初始化逻辑的主体。如果类有主要构造函数,则每个辅助构造函数都需要初始化该主要构造函数。

您可以使用主要构造函数来初始化类标头中的属性。传递给构造函数的实参会赋给属性。定义主要构造函数的语法是以类名称开头,后面依次跟 constructor 关键字和一对圆括号。圆括号中包含主要构造函数的形参。如果有多个形参,请用英文逗号分隔形参定义。定义主要构造函数的完整语法如下图所示:

aa05214860533041.png

辅助构造函数包含在类的主体中,其语法包括以下三个部分:

  • 辅助构造函数声明:辅助构造函数定义以 constructor 关键字开头,后跟圆括号。可视情况在圆括号中包含辅助构造函数所需的形参。
  • 主要构造函数初始化:初始化以冒号开头,后面依次跟 this 关键字和一对圆括号。可视情况在圆括号中包含主要构造函数所需的形参。
  • 辅助构造函数主体:在主要构造函数的初始化后跟一对大括号,其中包含辅助构造函数的主体。

语法如下图所示:

2dc13ef136009e98.png

例如,假设您想集成由智能设备提供商开发的 API。不过,该 API 会返回 Int 类型的状态代码来指明初始设备状态。如果设备处于离线状态,该 API 会返回 0 值;如果设备处于在线状态,则返回 1 值。对于任何其他整数值,系统会将状态视为“未知”。您可以在 SmartDevice 类中创建辅助构造函数,以将此 statusCode 形参转换为字符串表示形式,如以下代码段所示:

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. 实现类之间的关系

继承可让您基于其他类的特性和行为构建类。您可以借助这种强大机制编写可重复使用的代码,并在类之间建立关系。

例如,市面上有许多智能设备,如智能电视、智能灯和智能开关。当您以编程语言的形式表示智能设备时,这类设备会有一些通用属性(例如名称、类别和状态)和通用行为(例如能够开启或关闭)。

不过,每种智能设备开启或关闭的方式并不相同。例如,若要开启电视,您可能需要先开启显示屏,然后设置最近一次的已知音量和频道。相比之下,开灯则只需调高或调低亮度即可。

此外,每种智能设备都各自具备其他功能和操作供用户使用。例如,您可以在电视上调节音量和更换频道;在使用灯具时,您可以调节亮度或颜色。

简而言之,所有智能设备都有不同的功能,但也有一些共同的特性。您可以将这些通用特性复制到每个智能设备类中,也可以用继承的方式让代码可重复使用。

为此,您需要创建 SmartDevice 父级类,并定义这些通用属性和行为。然后,您可以创建子级类(例如 SmartTvDeviceSmartLightDevice 类)来继承父级类的属性。

用编程的术语来说,SmartTvDeviceSmartLightDevice 类会扩展 SmartDevice 父级类。父级类也称为父类,子级类则称为子类。这些类之间的关系如下图所示:

表示类之间继承关系的示意图。

不过,在 Kotlin 中,所有类默认都是最终类,也就是说您无法扩展这些类,因此必须定义类之间的关系。

定义 SmartDevice 父类及其子类之间的关系:

  1. SmartDevice 父类中的 class 关键字前面添加 open 关键字,使其具有扩展性:
open class SmartDevice(val name: String, val category: String) {
    ...
}

open 关键字会告知编译器此类可供扩展,因此其他类现在可对其进行扩展。

就像您迄今为止做的那样,创建子类的语法是从创建类标头开始。在构造函数的右圆括号后面,依次跟空格、冒号、另一个空格、父类名称以及一对圆括号。如有必要,可在圆括号中包含父类构造函数所需的形参。语法如下图所示:

1ac63b66e6b5c224.png

  1. 创建会扩展 SmartDevice 父类的 SmartTvDevice 子类:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

SmartTvDeviceconstructor 定义没有指定属性是可变的还是不可变的,这意味着,deviceNamedeviceCategory 形参只是 constructor 形参,而不是类属性。您无法在类中使用这些形参,只能将其传递给父类构造函数。

  1. SmartTvDevice 子类主体中,添加您在学习 getter 和 setter 函数时创建的 speakerVolume 属性:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. 定义被赋予 1 值的 channelNumber 属性,并包含指定 0..200 范围的 setter 函数:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
}
  1. 定义会调高音量并输出 "Speaker volume increased to $speakerVolume." 字符串的 increaseSpeakerVolume() 方法:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    } 
}