此文章是暑培的时候写的,但是实际上就是参考官网的教程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 中,变量的声明采用两种不同的关键字:val
和
var
。
val
用于声明不可变的变量,即一旦赋值后就不能再更改。var
用于声明可变的变量,即可以多次赋值。
例如,count
是一个 Int
类型的变量,初始值为
10
:
1 | var count: Int = 10 |
Int
是表示整数的类型之一。Kotlin 还提供了其他数值类型,如
Byte
、Short
、Long
、Float
和
Double
,以满足不同的数值需求。以下是这些类型的简要说明:
在 Kotlin 中,有一些常见的基本数据类型。下表逐行列明了各种不同的数据类型,并针对每种数据类型提供了可存储数据类型的说明和示例值。
Kotlin 数据类型 | 可包含的数据类型 | 字面量值示例 |
---|---|---|
String | 文本 | "Add contact" "Search" "Sign in" |
Int | 整数 | 32 1293490 -59281 |
Double | 小数 | 2.0 501.0292 -31723.99999 |
Float | 小数 (不如 Double 精确),数字末尾带有 f 或
F |
5.0f -1630.209f 1.2940278F |
Boolean | true 或 false 。当只有两个可能的值时,
可使用此数据类型。请注意,true 和 false 是
Kotlin 中的关键字。 |
true false |
使用 var
关键字意味着可以重新赋值给
count
。例如,可以将 count
的值从
10
改为 15
:
1 | var count: Int = 10 |
有些变量的值是不应该更改的。假设有一个名为 languageName
的 String
类型变量,如果希望确保 languageName
的值始终为 “Kotlin”,可以使用 val
关键字:
1 | val languageName: String = "Kotlin" |
通过使用 val
和
var
,可以明确区分哪些变量的值是可变的,哪些是不可变的。针对不同的需求做出适当的选择。如果变量的值需要改变,则使用
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 | /* |
类型推断
Kotlin
拥有强大的类型推断功能,可以根据初始值自动推断变量的类型,从而简化代码。延续前面的示例,在为
languageName
赋值后,Kotlin
编译器会根据初始值推断其类型。
由于 "Kotlin"
是 String
类型,因此编译器推断 languageName
也为
String
。 > 注意:Kotlin
是一种静态类型语言,这意味着类型在编译时解析并且不会改变。
在以下示例中,languageName
被推断为
String
,因此只能调用 String
类的方法:
1 | val languageName = "Kotlin" |
uppercase()
是一个用于将字符串中的小写字母转换为大写字母的 String
类型方法。由于 Kotlin 编译器已将 languageName
推断为
String
,因此可以成功调用
uppercase()
。然而,inc()
是一个
Int
类型的方法,用于将数值增加 1,因此不能对
String
类型调用它。通过 Kotlin
的类型推断,可以使代码更加简洁且类型安全。
条件语句
Kotlin 提供了几种实现条件逻辑的机制,其中最常见的是
if-else
语句。这种语句用于基于条件的真伪来执行不同的代码块。语法非常直观:如果
if
关键字后面的表达式求值为
true
,则执行该分支中的代码;否则,执行 else
分支中的代码。让我们看一个简单的示例:
1 | if (count == 42) { |
可以使用 else if
表示多个条件。这样,就可以在单个条件语句中表示更精细、更复杂的逻辑。例如:
1 | if (count == 42) { |
条件语句对于表示有状态的逻辑非常有用,但可能会发现,编写这些语句时会出现重复。在上面的示例中,每个分支都是输出一个
String
。为了避免这种重复,Kotlin
提供了条件表达式。最后一个示例可以重新编写如下:
1 | val answerString: String = if (count == 42) { |
每个条件分支都隐式地返回其最后一行的表达式的结果,因此无需使用
return
关键字。由于全部三个分支的结果都是
String
类型,因此 if-else
表达式的结果也是
String
类型。在本例中,根据 if-else
表达式的结果为 answerString
赋予了一个初始值。利用类型推断可以省略 answerString
的显式类型声明,但为了清楚起见,通常最好添加该声明。
注意:Kotlin 不包含传统的三元运算符 (
?:
),而是倾向于使用条件表达式。这使得代码更加简洁明了。
随着条件语句的复杂性不断增加,可以考虑将 if-else
表达式替换为 when
表达式,如以下示例所示:
1 | val answerString = when { |
when
表达式中每个分支都由一个条件、一个箭头
(->
) 和一个结果来表示。如果分支头左侧的条件求值为
true
,则返回右侧的表达式结果。请注意,执行并不是从一个分支跳转到下一个分支。when
表达式示例中的代码在功能上与上一个示例中的代码等效,但结构更为清晰。
此外,when
表达式不仅可以用于简单的条件判断,还可以用于更复杂的模式匹配。例如,可以根据变量的类型执行不同的操作:
1 | fun checkType(obj: Any) { |
循环结构
Kotlin 提供了几种实现循环的机制,其中最常见的是 for
循环和 while
循环。
for
循环
for
循环用于遍历集合、数组或区间,并对每个元素执行指定的操作。
1 | val numbers = listOf(1, 2, 3, 4, 5) |
在上面的示例中,numbers
是一个包含整数的列表。for
循环将依次遍历列表中的每个元素,并将当前元素赋值给 number
变量,然后执行循环体中的代码块。
可以使用 for
循环遍历任何实现了 Iterable
接口的对象,例如集合、数组或区间。以下是一些使用 for
循环的示例:
1 | // 遍历数组 |
在第一个示例中,for
循环遍历数组 array
中的每个元素,并将当前元素赋值给 element
变量。第二个示例展示了如何遍历一个从 1 到 5 的区间。
有时,不仅需要访问元素的值,还需要访问元素的索引。这时可以使用
withIndex
函数:
1 | val list = listOf("a", "b", "c") |
在上面的示例中,withIndex
函数返回一个包含索引和值的
IndexedValue
对象。通过解构声明,可以将索引和值分别赋值给
index
和 value
变量。
Kotlin 还提供了一种更简洁的方式来遍历区间:
1 | for (i in 1 until 5) { |
1 until 5
表示从 1 到 4 的半开区间,不包括
5。在循环中,i
的值依次为 1, 2, 3, 4。
如果需要以特定步长遍历区间,可以使用 step
函数:
1 | for (i in 1..10 step 2) { |
在上面的示例中,1..10 step 2
表示从 1 到 10
的区间,步长为 2。循环体中的 i
将依次取值 1, 3, 5, 7,
9。
此外,还可以使用 downTo
函数以递减顺序遍历区间:
1 | for (i in 10 downTo 1) { |
在这个示例中,10 downTo 1
表示从 10 到 1
的递减区间。for
循环可以高效地遍历任何类型的集合,并且可以与其他 Kotlin
特性(如解构声明和类型推断)结合使用,以进一步简化代码。
while
循环
while
循环用于在给定条件为真时重复执行一段代码。while
循环在每次迭代前都会检查条件,如果条件为真,则执行循环体中的代码。
1 | var count = 0 |
在上面的示例中,count
变量初始值为 0。while
循环在每次迭代前检查 count < 5
是否为真。如果为真,则执行循环体中的代码,输出当前 count
的值,并将 count
增加 1。循环将一直执行,直到
count
的值不再小于 5。
do-while
循环与 while
循环类似,但它会先执行一次循环体中的代码,然后再检查条件。这意味着
do-while
循环至少会执行一次。
1 | var count = 0 |
在上面的示例中,即使初始条件为假,do-while
循环也会执行一次循环体中的代码,然后再检查 count < 5
是否为真。
通过合理使用 for
循环和 while
循环,可以简洁、高效地遍历各种集合、数组和区间,并对每个元素执行所需的操作。这些循环结构在编写高效、可维护的代码时非常有用,特别是在处理大量数据或需要重复执行某些操作时。
函数
可以将相应的表达式封装在一个函数中并调用该函数,而不必在每次需要某个结果时都重复同一系列的表达式。
要声明函数,请使用 fun
关键字,后跟函数名称。定义函数时,可以指定希望其返回的值的数据类型。如果指定返回值类型,只需在圆括号后面添加冒号
(:
),然后在冒号后面添加一个空格和类型名称(例如
Int
、String
等)。然后,在返回值类型与左大括号之间添加一个空格。在函数主体中,可以在所有语句之后使用
return
语句指定希望函数返回的值。return
语句包含 return
关键字,后跟希望函数作为输出返回的值(例如变量)。
默认情况下,如果不指定返回值类型,默认返回值类型是
Unit
。Unit
表示函数并不会返回值。Unit
相当于其他语言中的 void
返回值类型(在 Java 和 C 中为 void
;在 Python 中为
None
等)。任何不返回值的函数都会隐式返回
Unit
。 1
2
3
4
5
6
7
8fun main() {
birthdayGreeting()
}
fun birthdayGreeting(): Unit {
println("Happy Birthday, Rover!")
println("You are now 5 years old!")
}
1 | fun generateAnswerString(): String { |
上面示例中的函数名为
generateAnswerString
。它不接受任何输入。它会输出
String
类型的结果。要调用函数,请使用函数名称,后跟调用运算符
(()
)。在下面的示例中,使用
generateAnswerString()
的结果对 answerString
变量进行了初始化。
1 | val answerString = generateAnswerString() |
函数可以接受参数输入,如以下示例所示:
1 | fun generateAnswerString(countThreshold: Int): String { |
在声明函数时,可以指定任意数量的参数及其类型。在上面的示例中,
generateAnswerString()
接受一个名为
countThreshold
的 Int
类型的参数。在函数中,可以使用参数的名称来引用参数。
调用此函数时,必须在函数调用的圆括号内添加一个参数:
1 | val answerString = generateAnswerString(42) |
简化函数声明
generateAnswerString()
是一个相当简单的函数。该函数声明一个变量,然后立即返回结果。函数返回单个表达式的结果时,可以通过直接返回函数中包含的
if-else
表达式的结果来跳过声明局部变量,如以下示例所示:
1 | fun generateAnswerString(countThreshold: Int): String { |
还可以将 return 关键字替换为赋值运算符:
1 | fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) { |
默认参数
Kotlin 支持为函数参数提供默认值,从而在调用函数时可以省略这些参数:
1
2
3
4
5
6fun greet(name: String = "World"): String {
return "Hello, $name!"
}
println(greet()) // 输出: Hello, World!
println(greet("Kotlin")) // 输出: Hello, Kotlin!
命名参数
在调用函数时,可以使用命名参数来明确指定每个参数的值。这在具有多个参数的函数中非常有用:
1
2
3
4
5
6
7fun 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
7fun main() {
val trickFunction = trick
}
fun trick() {
println("No treats!")
}
1 | Function invocation 'trick()' expected |
可以使用函数引用算符(::
),具体使用如下:
1
2
3
4
5
6
7fun main() {
val trickFunction = ::trick
}
fun trick() {
println("No treats!")
}
使用 lambda 表达式重新定义函数
lambda 表达式提供了简洁的语法来定义函数,无需使用 fun
关键字。可以直接将 lambda
表达式存储在变量中,无需对其他函数进行函数引用。
在赋值运算符(=
)前面,要添加 val
或
var
关键字,后跟变量名称,以供在调用函数时使用。赋值运算符(=
)后面是
lambda
表达式,它由一对大括号构成,而大括号中的内容则构成函数正文。语法如下图所示:
1 | val <variable name> = { |
使用 lambda 表达式定义函数时,有一个引用该函数的变量。还可以像对待任何其他类型一样,将其值分配给其他变量,并使用新变量的名称调用该函数。
1 | fun main() { |
系统会调用函数两次,一次针对 trick()
函数调用,第二次针对 trickFunction()
函数调用。
1 | No treats! |
高阶函数
一个函数可以将另一个函数当作参数。将其他函数用作参数的函数称为“高阶函数”。此模式对组件之间的通信(其方式与在 Java 中使用回调接口相同)很有用。
下面是一个高阶函数的示例:
1 | fun stringMapper(str: String, mapper: (String) -> Int): Int { |
stringMapper()
函数接受一个 String
以及一个函数,该函数根据传递给它的 String
来推导
Int
值。
要调用 stringMapper()
,可以传递一个 String
和一个满足其他输入参数的函数(即,一个将 String
当作输入并输出 Int
的函数),如以下示例所示:
1 | stringMapper("Android", { input -> |
如果匿名函数是在某个函数上定义的最后一个参数,则可以在用于调用该函数的圆括号之外传递它,如以下示例所示:
1
2
3stringMapper("Android") { input ->
input.length
}
在整个 Kotlin 标准库中可以找到很多匿名函数。如需了解详情,请参阅高阶函数和 Lambda。
类
到目前为止提到的所有类型都已内置在 Kotlin
编程语言中。如果希望添加自己的自定义类型,可以使用 class
关键字来定义,如以下示例所示:
1 | class Car |
类名称采用 PascalCase
大小写形式编写,因此每个单词都以大写字母开头,且各个单词之间没有空格。以“SmartDevice”为例,每个单词的第一个字母都大写,且单词之间没有空格。
属性
类使用属性来表示状态。属性是类级变量,可以包含 getter、setter
和后备字段。由于汽车需要轮子来驱动,因此可以添加 Wheel
对象的列表作为 Car
的属性,如以下示例所示:
1 | class Car { |
请注意,wheels
是一个
public val
,这意味着,可以从 Car
类外部访问
wheels
,并且不能为其重新赋值。如果要获取 Car
的实例,必须先调用其构造函数。这样,便可以访问它的任何可访问属性。对于这种初始化的方式必须要配一个初始值,否则无法通过编译。
1 | val car = Car() // construct a Car |
构造函数
默认构造函数
默认构造函数不含形参。定义默认构造函数的做法如以下代码段所示:
1 | class SmartDevice constructor() { |
Kotlin
旨在简化代码,因此,如果构造函数中没有任何注解或可见性修饰符,可以移除
constructor
关键字。如果构造函数中没有任何形参,还可以移除圆括号,如以下代码段所示:
1 | class SmartDevice { |
Kotlin 编译器会自动生成默认构造函数。不会在自己的代码中看到自动生成的默认构造函数,因为编译器会在后台进行添加。
定义形参化构造函数
在 SmartDevice
类中,name
和
category
属性不可变。需要确保 SmartDevice
类的所有实例初始化 name
和 category
属性。在当前实现中,name
和 category
属性的值都采用硬编码。也就是说,所有智能设备都是以
"Android TV"
字符串命名,并使用
"Entertainment"
字符串进行分类。
若要保持不变性,同时避免使用硬编码值,可以使用形参化构造函数进行初始化:
- 在
SmartDevice
类中,将name
和category
属性移至构造函数中,且不赋予默认值:
1 | class SmartDevice(val name: String, val category: String) { |
现在,该构造函数可接受形参来设置其属性,因此,为此类实例化对象的方式也会随之更改。实例化对象的完整语法如下图所示:
代码表示形式如下:
1 | SmartDevice("Android TV", "Entertainment") |
构造函数的这两个实参都是字符串,因此我们不清楚应该为哪个形参赋值。解决此问题的做法与传递函数实参的方式类似,只需创建包含具名实参的构造函数即可,如以下代码段所示:
1 | SmartDevice(name = "Android TV", category = "Entertainment") |
Kotlin 中的构造函数主要有两类:
- 主要构造函数:一个类只能有一个主要构造函数(在类标头中定义)。主要构造函数可以是默认构造函数,也可以是形参化构造函数。主要构造函数没有主体,表示其中不能包含任何代码。
- 辅助构造函数:一个类可以有多个辅助构造函数。可以定义包含形参或不含形参的辅助构造函数。辅助构造函数可以初始化类,具有包含初始化逻辑的主体。如果类有主要构造函数,则每个辅助构造函数都需要初始化该主要构造函数。
辅助构造函数包含在类的主体中,其语法包括以下三个部分:
- 辅助构造函数声明:辅助构造函数定义以
constructor
关键字开头,后跟圆括号。可视情况在圆括号中包含辅助构造函数所需的形参。 - 主要构造函数初始化:初始化以冒号开头,后面依次跟
this
关键字和一对圆括号。可视情况在圆括号中包含主要构造函数所需的形参。 - 辅助构造函数主体:在主要构造函数的初始化后跟一对大括号,其中包含辅助构造函数的主体。
语法如下所示: 这里的this就是调用当前类的主构造函数。
例如,假设想集成由智能设备提供商开发的 API。不过,该 API 会返回
Int
类型的状态代码来指明初始设备状态。如果设备处于离线状态,该 API 会返回
0
值;如果设备处于在线状态,则返回 1
值。对于任何其他整数值,系统会将状态视为“未知”。可以在
SmartDevice
类中创建辅助构造函数,以将此
statusCode
形参转换为字符串表示形式,如以下代码段所示:
1 | class SmartDevice(val name: String, val category: String) { |
Getter
Getter
方法用于获取对象的属性值。它通常是一个没有参数的方法,当调用它时,它返回对象的某个属性的值。Getter
方法名通常以 get
开头,后接属性的名称。
示例: 1
2
3
4
5
6
7
8
9class 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
12class 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 | class Car { |
在这个示例中,brand
属性有自定义的 getter 和 setter
方法,这些方法在访问和修改属性值时会打印日志信息。
通过使用 getter 和 setter 方法,可以在访问和修改对象属性时添加额外的逻辑,例如数据验证、日志记录等。
使用后备字段
Kotlin 自动为每个属性生成一个后备字段,称为
field
。可以在 getter 和 setter 中使用 field
来访问和修改属性的值。以下是一个示例:
1 | class Person { |
在以上示例中,age
属性的 setter 方法包含一个检查,确保
age
非负。如果尝试设置负数,会打印一条错误消息。
封装
封装是面向对象编程的核心概念之一。在 Kotlin 中,封装通过将对象的状态(属性)和行为(方法)封装在类中,并控制对这些属性和方法的访问来实现。封装有助于保护对象的内部状态,并提供一种受控的方式来访问或修改该状态。
属性的可见性
Kotlin 提供了四种可见性修饰符来控制属性和方法的访问权限:
public
:默认修饰符,任何地方都可以访问。internal
:在模块内可见。protected
:在类及其子类内可见。private
:仅在类内可见。
以下是一个示例,展示了如何使用可见性修饰符:
1 | class Car { |
在以上示例中: - engine
属性是 private
的,仅在 Car
类内可见。 - wheels
属性是
protected
的,在 Car
类及其子类内可见。 -
brand
属性是 internal
的,在同一模块内可见。 -
model
属性是 public
的,任何地方都可以访问。
模块: 模块 是源文件和构建设置的集合,可让将项目划分为独立的功能单元的项目可以包含一个或多个模块。可以独立构建、测试和调试每个模块。
软件包就像是用来对相关类进行分组的目录或文件夹,模块则是用来为应用的源代码、资源文件和应用级设置提供容器。一个模块可以包含多个软件包。
类似的,我们可以为方法,构造函数,类指定可见性修饰符 ##### Setter 和 Getter 的可见性
可以分别为属性的 getter 和 setter 指定不同的可见性修饰符。以下是一个示例:
1 | class Person { |
在以上示例中,name
属性的 setter 方法是
private
的,因此只能在 Person
类内部修改。
数据类
Kotlin 提供了数据类来简化封装数据的工作。数据类自动生成了
equals
、hashCode
、toString
、copy
等方法。以下是一个示例:
1 | data class User(val name: String, val age: Int) |
在以上示例中,User
是一个数据类,自动生成了常用方法,使得数据封装更加简洁。
继承
在 Kotlin 中,继承是实现代码重用和构建层次结构的关键机制。通过继承,一个类可以继承另一个类的属性和方法,从而扩展其功能。
基本概念
在 Kotlin 中,所有类默认都是 final
的,这意味着它们不能被继承。如果希望一个类可以被继承,需要使用
open
关键字来修饰该类。
1 | open class Vehicle { |
创建子类
子类使用 :
符号继承父类,并调用父类的构造函数。以下示例展示了如何创建一个继承自
Vehicle
类的 Car
类:
1 | open class Vehicle(val name: String, val brand: String) |
在以上示例中,Car
类继承了 Vehicle
类,并扩展了一个新的属性 seats
。
覆盖属性和方法
子类可以覆盖父类的属性和方法。要覆盖属性或方法,需要使用
override
关键字。以下是一个示例:
1 | open class Vehicle { |
在以上示例中,Car
类覆盖了 Vehicle
类的
speed
属性和 accelerate
方法。
使用 super
关键字
super
关键字用于调用父类的属性和方法。以下示例展示了如何在子类中使用
super
关键字:
1 | open class Vehicle { |
在以上示例中,Car
类的 start
方法调用了
Vehicle
类的 start
方法,然后执行了自己的逻辑。
继承和构造函数
在继承中,子类的构造函数需要调用父类的构造函数。以下是一个示例:
1 | open class Vehicle(val name: String) |
在以上示例中,Car
类的构造函数调用了
Vehicle
类的构造函数。
抽象类
抽象类是不能被实例化的类,通常用作基类。抽象类可以包含抽象成员(即没有实现的方法),需要在子类中实现这些成员。以下是一个示例:
1 | abstract class Vehicle { |
在以上示例中,Vehicle
类是一个抽象类,包含一个抽象属性
speed
和一个抽象方法
accelerate
。Car
类继承自 Vehicle
并实现了这些抽象成员。
接口
接口是一种特殊的类,可以包含抽象方法和具体方法。类可以实现一个或多个接口。以下是一个示例:
1 | interface Drivable { |
在以上示例中,Drivable
接口定义了一个属性
maxSpeed
和一个方法 drive
。Car
类实现了 Drivable
接口,并提供了这些成员的实现。
多继承
Kotlin 不支持多继承,但可以通过实现多个接口来实现类似的功能。以下是一个示例:
1 | interface Drivable { |
在以上示例中,FlyingCar
类实现了 Drivable
和 Flyable
接口。
Null 安全
在某些编程语言中,可以声明引用类型变量而不提供初始值,这些变量通常包含
null
值。默认情况下,Kotlin 中的变量不能持有
null
值。这意味着以下代码段是无效的:
1 | // 编译失败 |
如果需要变量持有 null
值,必须显式将其声明为可为
null
类型。可以在类型后面加上
?
,将变量指定为可为 null
,如下所示:
1 | val languageName: String? = null |
指定为 String?
类型后,可以为 languageName
赋值为 String
或 null
。
使用可为 null 的变量
初始化为null:
1 | fun main() { |
重新赋值为null
错误例子: 1
2
3
4fun main() {
var favoriteSinger: String = "milet"
favoriteSinger = null
}1
2
3
4
5
6
7fun main() {
var favoriteSinger: String? = "milet"
println(favoriteSinger)
favoriteSinger = null
println(favoriteSinger)
}
处理可为 null 的变量
继续上面的例子,如果我们想要访问可以为null的变量的属性,一个错误的例子如下:
1
2
3
4fun main() {
var favoriteSinger: String? = "milet"
println(favoriteSinger.length)
} 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
变量的属性,请按以下步骤操作:
- 在
println()
语句中,将.
运算符替换为?.
安全调用运算符:
1 | fun main() { |
- 运行此程序,结果为:
1 | 5 |
- 将
favoriteSinger
变量重新赋予null
,然后运行此程序:1
2
3
4fun main() {
var favoriteSinger: String? = null
println(favoriteSinger?.length)
} - 运行此程序,结果为:
1 | null |
也就说,如果尝试访问null
变量的length
属性,该程序也不会崩溃,只会返回null
使用 !!
非 null
断言运算符
还可以使用 !!
非 null 断言运算符来访问可为 null
的变量的方法或属性。
1 | <nullable variable> !!. <method/property> |
需要在可为 null 的变量后面添加 !!
非 null
断言运算符,之后再跟 .
运算符,最后添加不含任何空格的方法或属性。顾名思义,如果使用
!!
非 null 断言运算符,即表示断言变量的值不是
null
,无论该变量是否为该值都应如此。
与 ?.
安全调用运算符不同,当可为 null 的变量确实为
null
时,使用 !!
非 null
断言运算符可能会导致系统抛出 NullPointerException
错误。因此,只有在变量始终为不可为 null
或设置了适当的异常处理时,才应使用该断言运算符。如果异常未得到处理,便会导致运行时错误。
如需使用 !!
非 null
断言运算符访问
favoriteSinger
变量的属性,请按以下步骤操作:
- 为
favoriteSinger
变量重新赋予喜爱歌手的名称,然后在println()
语句中将?.
安全调用运算符替换为!!
非 null 断言运算符:
1 | fun main() { |
- 运行此程序,然后验证输出是否符合预期:
1 | 5 |
- 为
favoriteSinger
变量重新赋予null
,然后运行此程序:
1 | fun main() { |
系统会显示 NullPointerException
错误,内容如下:

此 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 | fun main() { |
预期的输出如下所示:
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 | fun main() { |
为 favoriteSinger
变量赋予
null
,然后运行此程序:
1 | fun main() { |
输出符合预期,如下所示:
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 | fun main() { |
输出会与之前的输出相同:
1 | The number of characters in your favorite singer's name is 5. |