0%

Kotlin 介绍

此文章是暑培的时候写的,但是实际上就是参考官网的教程copy了一份。 ## Kotlin 介绍

Kotlin 是一种现代且成熟的编程语言,由 JetBrains 公司于 2011 年设计和开发,并在 2016 年正式发布。在 2019 年的 Google I/O 大会上,Google 宣布 Kotlin 将成为 Android 开发的首选语言。包括 Google 地图、Google Home、Play、Google 云端硬盘和 Google 信息等应用。其中一个成功案例来自 Google Home 团队,他们将新功能开发工作迁移到 Kotlin 后,代码库大小减少了 33%,NPE 崩溃次数减少了 30%。Kotlin 以其简洁性和强大的功能而闻名,不仅能减少常见的代码错误,还能轻松集成到现有的应用程序中。如果打算构建 Android 应用,建议从 Kotlin 开始,充分利用其一流的功能。

开发者倾向于使用 Kotlin 的原因主要有以下几个:

  • 富有表现力且简洁:使用更少的代码实现更多的功能,减少样板代码,表达更加清晰。
  • 更安全的代码:Kotlin 提供许多语言特性,帮助避免空指针异常等常见的编程错误。
  • 高度互操作:Kotlin 与 Java 完全互操作,可以在 Kotlin 代码中调用 Java 代码,或在 Java 代码中调用 Kotlin 代码,让可以在项目中无缝添加 Kotlin 代码。

Kotlin 语言基础

此教程主要用于Kotlin速成,快速上手。 在线kotlin编辑器 ### 变量声明

在 Kotlin 中,变量的声明采用两种不同的关键字:valvar

  • val 用于声明不可变的变量,即一旦赋值后就不能再更改。
  • var 用于声明可变的变量,即可以多次赋值。

例如,count 是一个 Int 类型的变量,初始值为 10

1
var count: Int = 10

alt text Int 是表示整数的类型之一。Kotlin 还提供了其他数值类型,如 ByteShortLongFloatDouble,以满足不同的数值需求。以下是这些类型的简要说明:

在 Kotlin 中,有一些常见的基本数据类型。下表逐行列明了各种不同的数据类型,并针对每种数据类型提供了可存储数据类型的说明和示例值。

Kotlin 数据类型 可包含的数据类型 字面量值示例
String 文本 "Add contact"
"Search"
"Sign in"
Int 整数 32
1293490
-59281
Double 小数 2.0
501.0292
-31723.99999
Float 小数 (不如 Double 精确),数字末尾带有 fF 5.0f
-1630.209f
1.2940278F
Boolean truefalse。当只有两个可能的值时, 可使用此数据类型。请注意,truefalse 是 Kotlin 中的关键字。 true
false

使用 var 关键字意味着可以重新赋值给 count。例如,可以将 count 的值从 10 改为 15

1
2
var count: Int = 10
count = 15

有些变量的值是不应该更改的。假设有一个名为 languageNameString 类型变量,如果希望确保 languageName 的值始终为 “Kotlin”,可以使用 val 关键字:

1
val languageName: String = "Kotlin"

通过使用 valvar,可以明确区分哪些变量的值是可变的,哪些是不可变的。针对不同的需求做出适当的选择。如果变量的值需要改变,则使用 var;否则,使用 val。 #### 编码规范 一些格式设置和编码规范:

  • 变量名称应采用驼峰式大小写形式,并以小写字母开头。
  • 在变量声明中指定数据类型时,应在冒号后面添加一个空格。
  • 赋值运算符(=)、加号(+)、减号(-)、乘号(*)、除号(/)等运算符的前后应有空格。

注释

编写代码时,另一个建议遵循的较好做法是添加注释来说明代码的用途。这样做可帮助读者更轻松地理解代码。可以使用两个正斜杠符号 (//) 来指明将相应行中该符号后的剩余文本视为注释,而不要解释为代码。通常的做法是在两个正斜杠符号后添加一个空格。

1
// This is a comment.

也可以在一行代码的中间位置插入注释。在下面的示例中,height = 1 是正常的编码语句。// 之后的所有内容(即 Assume the height is 1 to start with)都会被解释为注释,而不会被视为代码。

1
height = 1 // Assume the height is 1 to start with.

对于多行注释而言。具体方法为:使用由正斜杠 (/) 和星号 (*) 组成的 /* 来作为多行注释的开头,在注释的每个新行开头添加一个星号,最后使用由星号和正斜杠符号组成的 */ 作为结尾。

1
2
3
4
/*
* This is a very long comment that can
* take up multiple lines.
*/

类型推断

Kotlin 拥有强大的类型推断功能,可以根据初始值自动推断变量的类型,从而简化代码。延续前面的示例,在为 languageName 赋值后,Kotlin 编译器会根据初始值推断其类型。

由于 "Kotlin"String 类型,因此编译器推断 languageName 也为 String。 > 注意:Kotlin 是一种静态类型语言,这意味着类型在编译时解析并且不会改变。

在以下示例中,languageName 被推断为 String,因此只能调用 String 类的方法:

1
2
3
4
5
6
val languageName = "Kotlin"
// 成功
val upperCaseName = languageName.uppercase()

// 编译失败
languageName.inc()

uppercase() 是一个用于将字符串中的小写字母转换为大写字母的 String 类型方法。由于 Kotlin 编译器已将 languageName 推断为 String,因此可以成功调用 uppercase()。然而,inc() 是一个 Int 类型的方法,用于将数值增加 1,因此不能对 String 类型调用它。通过 Kotlin 的类型推断,可以使代码更加简洁且类型安全。

条件语句

Kotlin 提供了几种实现条件逻辑的机制,其中最常见的是 if-else 语句。这种语句用于基于条件的真伪来执行不同的代码块。语法非常直观:如果 if 关键字后面的表达式求值为 true,则执行该分支中的代码;否则,执行 else 分支中的代码。让我们看一个简单的示例:

1
2
3
4
5
if (count == 42) {
println("I have the answer.")
} else {
println("The answer eludes me.")
}

可以使用 else if 表示多个条件。这样,就可以在单个条件语句中表示更精细、更复杂的逻辑。例如:

1
2
3
4
5
6
7
if (count == 42) {
println("I have the answer.")
} else if (count > 35) {
println("The answer is close.")
} else {
println("The answer eludes me.")
}

条件语句对于表示有状态的逻辑非常有用,但可能会发现,编写这些语句时会出现重复。在上面的示例中,每个分支都是输出一个 String。为了避免这种重复,Kotlin 提供了条件表达式。最后一个示例可以重新编写如下:

1
2
3
4
5
6
7
8
9
10
val answerString: String = if (count == 42) {
println("I have the answer.")
"I have the answer."
} else if (count > 35) {
"The answer is close."
} else {
"The answer eludes me."
}

println(answerString)

每个条件分支都隐式地返回其最后一行的表达式的结果,因此无需使用 return 关键字。由于全部三个分支的结果都是 String 类型,因此 if-else 表达式的结果也是 String 类型。在本例中,根据 if-else 表达式的结果为 answerString 赋予了一个初始值。利用类型推断可以省略 answerString 的显式类型声明,但为了清楚起见,通常最好添加该声明。

注意:Kotlin 不包含传统的三元运算符 (?:),而是倾向于使用条件表达式。这使得代码更加简洁明了。

随着条件语句的复杂性不断增加,可以考虑将 if-else 表达式替换为 when 表达式,如以下示例所示:

1
2
3
4
5
6
7
val answerString = when {
count == 42 -> "I have the answer."
count > 35 -> "The answer is close."
else -> "The answer eludes me."
}

println(answerString)

when 表达式中每个分支都由一个条件、一个箭头 (->) 和一个结果来表示。如果分支头左侧的条件求值为 true,则返回右侧的表达式结果。请注意,执行并不是从一个分支跳转到下一个分支。when 表达式示例中的代码在功能上与上一个示例中的代码等效,但结构更为清晰。

此外,when 表达式不仅可以用于简单的条件判断,还可以用于更复杂的模式匹配。例如,可以根据变量的类型执行不同的操作:

1
2
3
4
5
6
7
fun checkType(obj: Any) {
when (obj) {
is String -> println("This is a String of length ${obj.length}")
is Int -> println("This is an Integer with value $obj")
else -> println("Unknown type")
}
}

循环结构

Kotlin 提供了几种实现循环的机制,其中最常见的是 for 循环和 while 循环。

for 循环

for 循环用于遍历集合、数组或区间,并对每个元素执行指定的操作。

1
2
3
4
val numbers = listOf(1, 2, 3, 4, 5)
for (number in numbers) {
println(number)
}

在上面的示例中,numbers 是一个包含整数的列表。for 循环将依次遍历列表中的每个元素,并将当前元素赋值给 number 变量,然后执行循环体中的代码块。

可以使用 for 循环遍历任何实现了 Iterable 接口的对象,例如集合、数组或区间。以下是一些使用 for 循环的示例:

1
2
3
4
5
6
7
8
9
10
// 遍历数组
val array = arrayOf("a", "b", "c")
for (element in array) {
println(element)
}

// 遍历区间
for (i in 1..5) {
println(i)
}

在第一个示例中,for 循环遍历数组 array 中的每个元素,并将当前元素赋值给 element 变量。第二个示例展示了如何遍历一个从 1 到 5 的区间。

有时,不仅需要访问元素的值,还需要访问元素的索引。这时可以使用 withIndex 函数:

1
2
3
4
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
println("Element at $index is $value")
}

在上面的示例中,withIndex 函数返回一个包含索引和值的 IndexedValue 对象。通过解构声明,可以将索引和值分别赋值给 indexvalue 变量。

Kotlin 还提供了一种更简洁的方式来遍历区间:

1
2
3
for (i in 1 until 5) {
println(i)
}

1 until 5 表示从 1 到 4 的半开区间,不包括 5。在循环中,i 的值依次为 1, 2, 3, 4。

如果需要以特定步长遍历区间,可以使用 step 函数:

1
2
3
for (i in 1..10 step 2) {
println(i)
}

在上面的示例中,1..10 step 2 表示从 1 到 10 的区间,步长为 2。循环体中的 i 将依次取值 1, 3, 5, 7, 9。

此外,还可以使用 downTo 函数以递减顺序遍历区间:

1
2
3
for (i in 10 downTo 1) {
println(i)
}

在这个示例中,10 downTo 1 表示从 10 到 1 的递减区间。for 循环可以高效地遍历任何类型的集合,并且可以与其他 Kotlin 特性(如解构声明和类型推断)结合使用,以进一步简化代码。

while 循环

while 循环用于在给定条件为真时重复执行一段代码。while 循环在每次迭代前都会检查条件,如果条件为真,则执行循环体中的代码。

1
2
3
4
5
var count = 0
while (count < 5) {
println("Count is $count")
count++
}

在上面的示例中,count 变量初始值为 0。while 循环在每次迭代前检查 count < 5 是否为真。如果为真,则执行循环体中的代码,输出当前 count 的值,并将 count 增加 1。循环将一直执行,直到 count 的值不再小于 5。

do-while 循环与 while 循环类似,但它会先执行一次循环体中的代码,然后再检查条件。这意味着 do-while 循环至少会执行一次。

1
2
3
4
5
var count = 0
do {
println("Count is $count")
count++
} while (count < 5)

在上面的示例中,即使初始条件为假,do-while 循环也会执行一次循环体中的代码,然后再检查 count < 5 是否为真。

通过合理使用 for 循环和 while 循环,可以简洁、高效地遍历各种集合、数组和区间,并对每个元素执行所需的操作。这些循环结构在编写高效、可维护的代码时非常有用,特别是在处理大量数据或需要重复执行某些操作时。

函数

可以将相应的表达式封装在一个函数中并调用该函数,而不必在每次需要某个结果时都重复同一系列的表达式。

要声明函数,请使用 fun 关键字,后跟函数名称。定义函数时,可以指定希望其返回的值的数据类型。如果指定返回值类型,只需在圆括号后面添加冒号 (:),然后在冒号后面添加一个空格和类型名称(例如 IntString 等)。然后,在返回值类型与左大括号之间添加一个空格。在函数主体中,可以在所有语句之后使用 return 语句指定希望函数返回的值。return 语句包含 return 关键字,后跟希望函数作为输出返回的值(例如变量)。

默认情况下,如果不指定返回值类型,默认返回值类型是 UnitUnit 表示函数并不会返回值。Unit 相当于其他语言中的 void 返回值类型(在 Java 和 C 中为 void;在 Python 中为 None 等)。任何不返回值的函数都会隐式返回 Unit

1
2
3
4
5
6
7
8
fun main() {
birthdayGreeting()
}

fun birthdayGreeting(): Unit {
println("Happy Birthday, Rover!")
println("You are now 5 years old!")
}

1
2
3
4
5
6
7
8
9
10
fun generateAnswerString(): String {
val count: Int = 43
val answerString = if (count == 42) {
"I have the answer."
} else {
"The answer eludes me"
}

return answerString
}

上面示例中的函数名为 generateAnswerString。它不接受任何输入。它会输出 String类型的结果。要调用函数,请使用函数名称,后跟调用运算符 (())。在下面的示例中,使用 generateAnswerString() 的结果对 answerString 变量进行了初始化。

1
val answerString = generateAnswerString()

函数可以接受参数输入,如以下示例所示:

1
2
3
4
5
6
7
8
fun generateAnswerString(countThreshold: Int): String {
val count: Int = 43
return if (count > countThreshold) {
"I have the answer."
} else {
"The answer eludes me."
}
}

在声明函数时,可以指定任意数量的参数及其类型。在上面的示例中, generateAnswerString() 接受一个名为 countThresholdInt 类型的参数。在函数中,可以使用参数的名称来引用参数。

调用此函数时,必须在函数调用的圆括号内添加一个参数:

1
val answerString = generateAnswerString(42)

简化函数声明

generateAnswerString() 是一个相当简单的函数。该函数声明一个变量,然后立即返回结果。函数返回单个表达式的结果时,可以通过直接返回函数中包含的 if-else 表达式的结果来跳过声明局部变量,如以下示例所示:

1
2
3
4
5
6
7
fun generateAnswerString(countThreshold: Int): String {
return if (count > countThreshold) {
"I have the answer."
} else {
"The answer eludes me."
}
}

还可以将 return 关键字替换为赋值运算符:

1
2
3
4
5
fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
"I have the answer"
} else {
"The answer eludes me"
}

默认参数

Kotlin 支持为函数参数提供默认值,从而在调用函数时可以省略这些参数:

1
2
3
4
5
6
fun greet(name: String = "World"): String {
return "Hello, $name!"
}

println(greet()) // 输出: Hello, World!
println(greet("Kotlin")) // 输出: Hello, Kotlin!

命名参数

在调用函数时,可以使用命名参数来明确指定每个参数的值。这在具有多个参数的函数中非常有用:

1
2
3
4
5
6
7
fun displayMessage(name: String, age: Int, city: String) {
println("$name, $age years old, from $city")
}

// 使用命名参数
displayMessage(name = "Alice", age = 30, city = "New York")
displayMessage(city = "Los Angeles", name = "Bob", age = 25)
通过命名参数,可以在调用函数时以任意顺序传递参数。这种方式尤其在参数较多或者参数具有默认值时显得非常有用。

将函数存储在变量中

直观上,我们会这样做,但实际上这个是一个错误的例子:

1
2
3
4
5
6
7
fun main() {
val trickFunction = trick
}

fun trick() {
println("No treats!")
}
因为 Kotlin 编译器会将 trick 识别为 trick() 函数的名称,但它想让调用该函数,而不是将其分配给变量。

1
Function invocation 'trick()' expected

可以使用函数引用算符(::),具体使用如下:

1
2
3
4
5
6
7
fun main() {
val trickFunction = ::trick
}

fun trick() {
println("No treats!")
}

使用 lambda 表达式重新定义函数

lambda 表达式提供了简洁的语法来定义函数,无需使用 fun 关键字。可以直接将 lambda 表达式存储在变量中,无需对其他函数进行函数引用。

在赋值运算符(=)前面,要添加 valvar 关键字,后跟变量名称,以供在调用函数时使用。赋值运算符(=)后面是 lambda 表达式,它由一对大括号构成,而大括号中的内容则构成函数正文。语法如下图所示:

1
2
3
val <variable name> = {
<function body>
}

使用 lambda 表达式定义函数时,有一个引用该函数的变量。还可以像对待任何其他类型一样,将其值分配给其他变量,并使用新变量的名称调用该函数。

1
2
3
4
5
6
7
8
9
fun main() {
val trickFunction = trick
trick()
trickFunction()
}

val trick = {
println("No treats!")
}

系统会调用函数两次,一次针对 trick() 函数调用,第二次针对 trickFunction() 函数调用。

1
2
No treats!
No treats!

高阶函数

一个函数可以将另一个函数当作参数。将其他函数用作参数的函数称为“高阶函数”。此模式对组件之间的通信(其方式与在 Java 中使用回调接口相同)很有用。

下面是一个高阶函数的示例:

1
2
3
4
fun stringMapper(str: String, mapper: (String) -> Int): Int {
// Invoke function
return mapper(str)
}

stringMapper() 函数接受一个 String 以及一个函数,该函数根据传递给它的 String 来推导 Int 值。

要调用 stringMapper(),可以传递一个 String 和一个满足其他输入参数的函数(即,一个将 String 当作输入并输出 Int 的函数),如以下示例所示:

1
2
3
stringMapper("Android", { input ->
input.length
})

如果匿名函数是在某个函数上定义的最后一个参数,则可以在用于调用该函数的圆括号之外传递它,如以下示例所示:

1
2
3
stringMapper("Android") { input ->
input.length
}

在整个 Kotlin 标准库中可以找到很多匿名函数。如需了解详情,请参阅高阶函数和 Lambda

到目前为止提到的所有类型都已内置在 Kotlin 编程语言中。如果希望添加自己的自定义类型,可以使用 class 关键字来定义,如以下示例所示:

1
class Car

类名称采用 PascalCase 大小写形式编写,因此每个单词都以大写字母开头,且各个单词之间没有空格。以“SmartDevice”为例,每个单词的第一个字母都大写,且单词之间没有空格。

属性

类使用属性来表示状态。属性是类级变量,可以包含 getter、setter 和后备字段。由于汽车需要轮子来驱动,因此可以添加 Wheel 对象的列表作为 Car 的属性,如以下示例所示:

1
2
3
class Car {
val wheels = listOf<Wheel>()
}

请注意,wheels 是一个 public val,这意味着,可以从 Car 类外部访问 wheels,并且不能为其重新赋值。如果要获取 Car 的实例,必须先调用其构造函数。这样,便可以访问它的任何可访问属性。对于这种初始化的方式必须要配一个初始值,否则无法通过编译。

1
2
val car = Car() // construct a Car
val wheels = car.wheels // retrieve the wheels value from the Car
构造函数
默认构造函数

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

1
2
3
4
5
class SmartDevice constructor() {
val name = "Android TV"
val category = "Entertainment"
var deviceStatus = "online"
}

Kotlin 旨在简化代码,因此,如果构造函数中没有任何注解或可见性修饰符,可以移除 constructor 关键字。如果构造函数中没有任何形参,还可以移除圆括号,如以下代码段所示:

1
2
3
4
5
class SmartDevice {
val name = "Android TV"
val category = "Entertainment"
var deviceStatus = "online"
}

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

定义形参化构造函数

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

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

  • SmartDevice 类中,将 namecategory 属性移至构造函数中,且不赋予默认值:
1
2
3
4
5
6
7
8
9
10
11
12
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.")
}
}

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

代码表示形式如下:

1
SmartDevice("Android TV", "Entertainment")

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

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

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

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

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

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

语法如下所示: alt text 这里的this就是调用当前类的主构造函数。

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

1
2
3
4
5
6
7
8
9
10
11
12
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"
}
}
...
}
Getter

Getter 方法用于获取对象的属性值。它通常是一个没有参数的方法,当调用它时,它返回对象的某个属性的值。Getter 方法名通常以 get 开头,后接属性的名称。

示例:

1
2
3
4
5
6
7
8
9
class Person {
var name: String = "Unknown"
get() = field // getter
}

fun main() {
val person = Person()
println(person.name) // 调用 getter 方法
}

在这个示例中,name 属性有一个默认的 getter 方法,它返回 field(即 name 属性的值)。

Setter

Setter 方法用于设置对象的属性值。它通常是一个接收一个参数的方法,该参数是要赋给属性的新值。Setter 方法名通常以 set 开头,后接属性的名称。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
var name: String = "Unknown"
set(value) {
field = value // setter
}
}

fun main() {
val person = Person()
person.name = "Alice" // 调用 setter 方法
println(person.name)
}

在这个示例中,name 属性有一个默认的 setter 方法,它接收一个参数 value,并将 field(即 name 属性的值)设置为这个参数。

以下是一个包含 getter 和 setter 的完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Car {
var brand: String = "Unknown"
get() {
println("Getting the brand")
return field
}
set(value) {
println("Setting the brand to $value")
field = value
}
}

fun main() {
val car = Car()
car.brand = "Toyota" // 调用 setter 方法
println(car.brand) // 调用 getter 方法
}

在这个示例中,brand 属性有自定义的 getter 和 setter 方法,这些方法在访问和修改属性值时会打印日志信息。

通过使用 getter 和 setter 方法,可以在访问和修改对象属性时添加额外的逻辑,例如数据验证、日志记录等。

使用后备字段

Kotlin 自动为每个属性生成一个后备字段,称为 field。可以在 getter 和 setter 中使用 field 来访问和修改属性的值。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
var age: Int = 0
set(value) {
if (value >= 0) {
field = value // 使用后备字段
} else {
println("Age cannot be negative")
}
}
}

fun main() {
val person = Person()
person.age = 25 // 调用 setter 方法
println(person.age) // 调用 getter 方法
person.age = -5 // 试图设置一个负数,将失败
}

在以上示例中,age 属性的 setter 方法包含一个检查,确保 age 非负。如果尝试设置负数,会打印一条错误消息。

封装

封装是面向对象编程的核心概念之一。在 Kotlin 中,封装通过将对象的状态(属性)和行为(方法)封装在类中,并控制对这些属性和方法的访问来实现。封装有助于保护对象的内部状态,并提供一种受控的方式来访问或修改该状态。

属性的可见性

Kotlin 提供了四种可见性修饰符来控制属性和方法的访问权限:

  • public:默认修饰符,任何地方都可以访问。
  • internal:在模块内可见。
  • protected:在类及其子类内可见。
  • private:仅在类内可见。

以下是一个示例,展示了如何使用可见性修饰符:

1
2
3
4
5
6
class Car {
private val engine = Engine()
protected val wheels = listOf<Wheel>()
internal val brand = "Toyota"
val model = "Corolla" // 默认是 public
}

在以上示例中: - engine 属性是 private 的,仅在 Car 类内可见。 - wheels 属性是 protected 的,在 Car 类及其子类内可见。 - brand 属性是 internal 的,在同一模块内可见。 - model 属性是 public 的,任何地方都可以访问。

模块模块 是源文件和构建设置的集合,可让将项目划分为独立的功能单元的项目可以包含一个或多个模块。可以独立构建、测试和调试每个模块。

软件包就像是用来对相关类进行分组的目录或文件夹,模块则是用来为应用的源代码、资源文件和应用级设置提供容器。一个模块可以包含多个软件包。

类似的,我们可以为方法,构造函数,类指定可见性修饰符 ##### Setter 和 Getter 的可见性

可以分别为属性的 getter 和 setter 指定不同的可见性修饰符。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
class Person {
var name: String = "Unknown"
private set // setter 是 private 的,只有类内部可以修改
}

fun main() {
val person = Person()
println(person.name) // 可以访问 getter
// person.name = "Alice" // 无法访问 setter
}

在以上示例中,name 属性的 setter 方法是 private 的,因此只能在 Person 类内部修改。

数据类

Kotlin 提供了数据类来简化封装数据的工作。数据类自动生成了 equalshashCodetoStringcopy 等方法。以下是一个示例:

1
2
3
4
5
6
7
data class User(val name: String, val age: Int)

fun main() {
val user = User("Alice", 30)
println(user.name) // 访问属性
println(user) // 调用 toString 方法
}

在以上示例中,User 是一个数据类,自动生成了常用方法,使得数据封装更加简洁。

继承

在 Kotlin 中,继承是实现代码重用和构建层次结构的关键机制。通过继承,一个类可以继承另一个类的属性和方法,从而扩展其功能。

基本概念

在 Kotlin 中,所有类默认都是 final 的,这意味着它们不能被继承。如果希望一个类可以被继承,需要使用 open 关键字来修饰该类。

1
2
3
open class Vehicle {
// 基类
}
创建子类

子类使用 : 符号继承父类,并调用父类的构造函数。以下示例展示了如何创建一个继承自 Vehicle 类的 Car 类:

1
2
3
open class Vehicle(val name: String, val brand: String)

class Car(name: String, brand: String, val seats: Int) : Vehicle(name, brand)

在以上示例中,Car 类继承了 Vehicle 类,并扩展了一个新的属性 seats

覆盖属性和方法

子类可以覆盖父类的属性和方法。要覆盖属性或方法,需要使用 override 关键字。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open class Vehicle {
open val speed: Int = 0

open fun accelerate() {
println("The vehicle is accelerating")
}
}

class Car : Vehicle() {
override val speed: Int = 100

override fun accelerate() {
println("The car is accelerating at $speed km/h")
}
}

在以上示例中,Car 类覆盖了 Vehicle 类的 speed 属性和 accelerate 方法。

使用 super 关键字

super 关键字用于调用父类的属性和方法。以下示例展示了如何在子类中使用 super 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
open class Vehicle {
open fun start() {
println("Vehicle is starting")
}
}

class Car : Vehicle() {
override fun start() {
super.start()
println("Car is starting")
}
}

在以上示例中,Car 类的 start 方法调用了 Vehicle 类的 start 方法,然后执行了自己的逻辑。

继承和构造函数

在继承中,子类的构造函数需要调用父类的构造函数。以下是一个示例:

1
2
3
open class Vehicle(val name: String)

class Car(name: String, val seats: Int) : Vehicle(name)

在以上示例中,Car 类的构造函数调用了 Vehicle 类的构造函数。

抽象类

抽象类是不能被实例化的类,通常用作基类。抽象类可以包含抽象成员(即没有实现的方法),需要在子类中实现这些成员。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class Vehicle {
abstract val speed: Int

abstract fun accelerate()
}

class Car : Vehicle() {
override val speed: Int = 100

override fun accelerate() {
println("The car is accelerating at $speed km/h")
}
}

在以上示例中,Vehicle 类是一个抽象类,包含一个抽象属性 speed 和一个抽象方法 accelerateCar 类继承自 Vehicle 并实现了这些抽象成员。

接口

接口是一种特殊的类,可以包含抽象方法和具体方法。类可以实现一个或多个接口。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Drivable {
val maxSpeed: Int

fun drive()
}

class Car : Drivable {
override val maxSpeed: Int = 120

override fun drive() {
println("The car is driving at a maximum speed of $maxSpeed km/h")
}
}

在以上示例中,Drivable 接口定义了一个属性 maxSpeed 和一个方法 driveCar 类实现了 Drivable 接口,并提供了这些成员的实现。

多继承

Kotlin 不支持多继承,但可以通过实现多个接口来实现类似的功能。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Drivable {
fun drive()
}

interface Flyable {
fun fly()
}

class FlyingCar : Drivable, Flyable {
override fun drive() {
println("The flying car is driving")
}

override fun fly() {
println("The flying car is flying")
}
}

在以上示例中,FlyingCar 类实现了 DrivableFlyable 接口。

Null 安全

在某些编程语言中,可以声明引用类型变量而不提供初始值,这些变量通常包含 null 值。默认情况下,Kotlin 中的变量不能持有 null 值。这意味着以下代码段是无效的:

1
2
// 编译失败
val languageName: String = null

如果需要变量持有 null 值,必须显式将其声明为可为 null 类型。可以在类型后面加上 ?,将变量指定为可为 null,如下所示:

1
val languageName: String? = null

指定为 String? 类型后,可以为 languageName 赋值为 Stringnull

使用可为 null 的变量

初始化为null:
1
2
3
fun main() {
val favoriteSinger = null
}
重新赋值为null

错误例子:

1
2
3
4
fun main() {
var favoriteSinger: String = "milet"
favoriteSinger = null
}
正确的写法:
1
2
3
4
5
6
7
fun main() {
var favoriteSinger: String? = "milet"
println(favoriteSinger)

favoriteSinger = null
println(favoriteSinger)
}

处理可为 null 的变量

继续上面的例子,如果我们想要访问可以为null的变量的属性,一个错误的例子如下:

1
2
3
4
fun main() {
var favoriteSinger: String? = "milet"
println(favoriteSinger.length)
}
这个时候我们会遇到这样的报错: alt text Kotlin 会刻意应用语法规则,以实现 null 安全,即保证不会意外调用 null 变量。但这并不表示变量不能为 null;而是表示,在访问某个变量的成员时,则该变量不能为 null

如果在应用运行时访问 null 变量的成员(称为 null 引用),应用会因 null 变量不含任何属性或方法而崩溃。此类崩溃称为“运行时错误”,即在代码完成编译和运行后发生的错误。

由于 Kotlin 具有 null 安全特性,因此 Kotlin 编译器会对可为 null 类型强制执行 null 检查,以免发生此类运行时错误。“Null 检查”是指在访问变量并将其视为不可为 null 类型之前,检查该变量是否为 null 的过程。如果想将可为 null 的值用作不可为 null 类型,则需要明确执行 null 检查。

在此示例中,系统不允许直接引用 favoriteSinger 变量的 length 属性,因为该变量有可能是 null,因此代码在编译时失败。

接下来,将会介绍用来处理可为 null 类型的各种技巧和运算符。 ##### 使用 ?. 安全调用运算符

可以使用 ?. 安全调用运算符访问可为 null 变量的方法或属性。

1
<nullable variable> ?. <method/property>

如需使用 ?. 安全调用运算符访问方法或属性,请在变量名称后面添加 ? 符号,并使用 . 表示法访问方法或属性。 ?. 安全调用运算符可让更安全地访问可为 null 的变量,因为 Kotlin 编译器会阻止变量成员为访问 null 引用而进行的任何尝试,并针对访问的成员返回 null

如需安全地访问可为 null 的 favoriteSinger 变量的属性,请按以下步骤操作:

  1. println() 语句中,将 . 运算符替换为 ?. 安全调用运算符:
1
2
3
4
fun main() {
var favoriteSinger: String? = "milet"
println(favoriteSinger?.length)
}
  1. 运行此程序,结果为:
1
5
  1. favoriteSinger 变量重新赋予 null,然后运行此程序:
    1
    2
    3
    4
    fun main() {
    var favoriteSinger: String? = null
    println(favoriteSinger?.length)
    }
  2. 运行此程序,结果为:
1
null

也就说,如果尝试访问null变量的length属性,该程序也不会崩溃,只会返回null

使用 !! 非 null 断言运算符

还可以使用 !! 非 null 断言运算符来访问可为 null 的变量的方法或属性。

1
<nullable variable> !!. <method/property>

需要在可为 null 的变量后面添加 !! 非 null 断言运算符,之后再跟 . 运算符,最后添加不含任何空格的方法或属性。顾名思义,如果使用 !! 非 null 断言运算符,即表示断言变量的值不是 null,无论该变量是否为该值都应如此。

?. 安全调用运算符不同,当可为 null 的变量确实为 null 时,使用 !! 非 null 断言运算符可能会导致系统抛出 NullPointerException 错误。因此,只有在变量始终为不可为 null 或设置了适当的异常处理时,才应使用该断言运算符。如果异常未得到处理,便会导致运行时错误。

如需使用 !!null 断言运算符访问 favoriteSinger 变量的属性,请按以下步骤操作:

  1. favoriteSinger 变量重新赋予喜爱歌手的名称,然后在 println() 语句中将 ?. 安全调用运算符替换为 !! 非 null 断言运算符:
1
2
3
4
fun main() {
var favoriteSinger: String? = "milet"
println(favoriteSinger!!.length)
}
  1. 运行此程序,然后验证输出是否符合预期:
1
5
  1. favoriteSinger 变量重新赋予 null,然后运行此程序:
1
2
3
4
fun main() {
var favoriteSinger: String? = null
println(favoriteSinger!!.length)
}

系统会显示 NullPointerException 错误,内容如下:

alt text

此 Kotlin 错误显示程序在执行期间崩溃。因此,除非确定变量不为 null,否则不建议使用 !! 非 null 断言运算符。

使用 if/else 条件

可以在 if/else 条件中使用 if 分支来执行 null 检查。

1
<nullable variable> != null

null 检查与 if/else 语句结合使用具有以下优点:

  • nullableVariable != null 表达式的 null 检查会被用作 if 条件。
  • if 分支中会假定变量不可为 null。因此,在这个主体中,可以随意访问变量的方法或属性,就像变量是不可为 null 一般,而不必使用 !! 安全调用运算符。
  • else 分支中会假定变量为 null。因此,在这个主体中,可以添加应在变量为 null 时运行的语句。else 分支是可选的。对 null 检查失败时,只能使用 if 条件来运行 null 检查,而不执行默认操作。

如果有多行代码使用可为 null 的变量,那么将 null 检查与 if 条件搭配使用会更方便。相比之下,?. 安全调用运算符更适用于对可为 null 变量的单次引用。

例子

1
2
3
4
5
6
7
fun main() {
var favoriteSinger: String? = "milet"

if (favoriteSinger != null) {
println("The number of characters in your favorite singer's name is ${favoriteSinger.length}")
}
}

预期的输出如下所示:

1
The number of characters in your favorite singer's name is 5.

请注意,由于会在 null 检查之后访问 if 分支中的 length 方法,因此可以直接使用 . 运算符访问名称的长度方法。同样,Kotlin 编译器知道 favoriteSinger 变量绝不可能为 null,因此允许直接访问属性。

可添加一个 else 分支,以处理歌手名称为 null 的情况:

else 分支的主体中,添加会接受 You didn't input a name. 字符串的 println 语句:

1
2
3
4
5
6
7
8
9
fun main() {
var favoriteSinger: String? = "milet"

if (favoriteSinger != null) {
println("The number of characters in your favorite singer's name is ${favoriteSinger.length}")
} else {
println("You didn't input a name.")
}
}

favoriteSinger 变量赋予 null,然后运行此程序:

1
2
3
4
5
6
7
8
9
fun main() {
var favoriteSinger: String? = null

if (favoriteSinger != null) {
println("The number of characters in your favorite singer's name is ${favoriteSinger.length}")
} else {
println("You didn't input a name.")
}
}

输出符合预期,如下所示:

1
You didn't input a name.
使用 ?: Elvis 运算符

?: Elvis 运算符可以与 ?. 安全调用运算符搭配使用。如果搭配使用 ?: Elvis 运算符,便可以在 ?. 安全调用运算符返回 null 时添加默认值。这与 if/else 表达式类似,但更为常用。

如果该变量不为 null,则执行 ?. Elvis 运算符之前的表达式;如果变量为 null,则执行 ?: Elvis 运算符之后的表达式。

1
val <name> = <nullable variable> ?. <method/property> ?: <default value>

length 属性之后,添加后跟的值 0?: Elvis 运算符,然后运行此程序:

1
2
3
4
5
6
7
fun main() {
val favoriteSinger: String? = "milet"

val lengthOfName = favoriteSinger?.length ?: 0

println("The number of characters in your favorite singer's name is $lengthOfName.")
}

输出会与之前的输出相同:

1
The number of characters in your favorite singer's name is 5.