# Kotlin教程 - 11 异常

# 11.1 异常的概念

什么是异常?

异常(Exceptions)是指在程序执行过程中出现的错误或异常情况,导致程序无法继续正常执行的事件。

Kotlin 提供了异常处理机制来捕获和处理这些异常。在 Kotlin 中,异常处理和 Java 类似,但也有一些独特之处。在 Kotlin 中,所有异常都是非受检异常(unchecked exception),这意味着你不需要在函数上声明可能抛出的异常。

例如:整数除数是0会抛出异常。

fun main() {
    var i: Int = 5 / 0
    println(i)

    println("异常后的代码")
}
1
2
3
4
5
6

执行的时候就会发生错误:

当程序出现错误的时候,我们通常称之为:抛出异常。

程序抛出异常就无法继续执行了,但是任何程序都不可能是完美的没有bug的,只能尽可能的对错误进行预防和处理。

# 11.2 捕获异常

对异常进行预防和提前处理,这种行为通常称之为异常捕获。

一个程序出现任何错误,就停止运行肯定不是我们希望看到的,即使程序出现错误,哪怕给与用户一个错误提示,也是一种积极的处理方式。

# 1 异常捕获语法

最简单的异常捕获语法:

try {
  // 可能会抛出异常的代码
} catch (e: Exception) {
  // 捕获并处理异常
}
1
2
3
4
5

举个栗子:

fun main() {
    try {
        var i: Int = 5 / 0
        println(i)
    } catch (e: Exception) {
        println("除数不能为0")
    }

    println("异常后的代码")
}
1
2
3
4
5
6
7
8
9
10

上面的程序执行到 5 / 0 会抛出异常,出现异常后面的语句无法继续执行,然后执行 catch 块中的语句。

但是在 try-catch 代码块后面的代码可以继续执行。

执行结果:

除数不能为0 异常后的代码

这样在发生错误的时候,可以给用户一个提示。当然具体的处理方式需要根据业务需求的处理,这里只是举个例子。

# 2 打印异常详细信息

捕获异常后,在catch块中,可以通过异常对象打印异常的详细信息。

fun main() {
    try {
        var i: Int = 5 / 0
        println(i)
    } catch (e: Exception) {
        println("除数不能为0")
        e.printStackTrace()
    }

    println("异常后的代码")
}
1
2
3
4
5
6
7
8
9
10
11

执行结果:

# 3 捕获指定类型的异常

在程序运行时,可能会出现不同类型的异常,可能需要对不同类型的异常进行不同的处理,这个时候就需要根据类型来捕获异常了。

举个栗子:

fun main() {
    try {
        var i: Int = 5 / 0
        println(i)
    }
    catch (e: ArithmeticException) {
        println("除数不能为0")
        e.printStackTrace()
    }

    println("异常后的代码")
}
1
2
3
4
5
6
7
8
9
10
11
12

上面捕获了 ArithmeticException 类型的异常,在捕获异常后,打印了异常信息和提示信息。

需要注意,上面只是捕获了 ArithmeticException 类型的异常,如果有代码抛出了其他类型的异常,是无法捕获的。

举个栗子,我们修改代码如下:

fun main() {
    try {
        var i: Int = 5 / 1
        println(i)

        var j = "abc".toInt()
        println(j)
    }
    catch (e: ArithmeticException) {
        println("除数不能为0")
        e.printStackTrace()
    }

    println("异常后的代码")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

此时的代码会抛出 NumberFormatException 异常,但是我们并没有捕获这个异常,导致程序被终止执行。

所以针对这种情况需要捕获多个异常。

# 4 捕获多个异常

我们可以同时捕获多个异常,并对每种异常可以采用不同的处理。

举个栗子:

fun main() {
    try {
        var i: Int = 5 / 0
        println(i)

        var j = "abc".toInt()
        println(j)
    }
    catch (e: ArithmeticException) {
        println("除数不能为0")
        e.printStackTrace()
    }
    catch (e: NumberFormatException) {
        println("数字格式不正确")
        e.printStackTrace()
    }
    catch (e: Exception) {
        e.printStackTrace()
    }

    println("异常后的代码")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在上面的代码中,我们针对两种异常进行了针对性的处理,但是如果此时程序还抛出其他异常,就会导致程序终止执行。为了处理这种情况,我们在最后面捕获了 Exception,它是所有异常的父类,这样如果有异常没有处理,就会被最后的 catch 捕获。

# 5 异常finally

finally表示的是无论是否发生异常都要执行的代码。

举个栗子:

fun main() {
    try {
        var i: Int = 5 / 0
        println(i)
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        println("出现异常也会被执行");
    }

    println("异常后的代码")
}
1
2
3
4
5
6
7
8
9
10
11
12

执行结果:

finally 一般在文件读写操作的时候用的比较多,就是不管是否发生错误都要关闭文件,所以可以将文件的关闭操作放在 finally 块中。

# 11.3 表达式风格的异常处理

在 Kotlin 中,try 表达式可以有返回值,因此你可以将其用作表达式。这使得异常处理更具表达性。

举个栗子:

fun main() {
    val result = try {
        5 / 0
    } catch (e: Exception) {
        0
    } finally {
        // 可选的 finally 块
    }
    println("Result: $result")
}
1
2
3
4
5
6
7
8
9
10

在上面的代码中,5 / 0 会抛出异常,则将返回 0 。如果 5 除以非 0,则返回计算的结果。

# 11.4 异常的传递

什么是异常的传递?

异常的传递,就是当函数或方法执行的时候出现异常,如果没有进行捕获,就会将异常传递给该函数或方法的调用者,如果调用者仍未处理异常,则继续向上传递,直到传递给主程序,如果主程序仍然没有进行异常处理,则程序将被终止。

举个栗子:

fun func() {
    var i: Int = 5 / 0
    println(i)
}

fun callFunc() {
    // 调用上面的func()
    func();
}

fun main() {
    try {
        callFunc();
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面代码,执行 5 / 0 会抛出异常,因为 func 函数中没有进行异常处理,则异常会抛给 callFunc() 函数,callFunc() 没有进行异常处理,则抛给 main() 函数,main() 函数中则对异常进行了处理,程序不会崩溃。

利用异常的传递性,如果我们在 main() 函数中进行了异常捕获,无论程序哪里发生了错误,最终都会被传递到 main() 函数中,保证所有的异常都会被捕获。但是不要所有的异常都在main() 函数中处理,main() 函数中的异常处理只是兜底处理。

# 11.5 主动抛出异常

除了代码执行的时候出错,系统会主动抛出异常,我们还可以根据实际的业务需要,主动抛出异常。

在 Kotlin 中就有一个内置函数 TODO() 该方法会主动抛出异常。

举个栗子:

fun sayHello() {
    TODO("还没学会说话")
}

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

TODO() 方法的返回值是 Nothing,抛出的是一个没有实现的异常。

执行结果:

如果我们要实现某个功能,但是还未实现,可以先使用 TODO() 方法来占位,代码运行到这里会报错,就知道还未实现。

我们也可以使用 throw 关键字抛出异常对象。

举个栗子:

fun inputUsername(): String {
    print("请输入用户名:");
    // 读取键盘输入
    val name = readLine()

    if (null == name || name.length > 16 || name.length < 8) {
        throw Exception("用户名格式错误"); // 主动抛出异常
    }

    return name;
}

fun main() {
    try {
        var password = inputUsername();
        print("输入的用户名:$password");
    } catch (e: Exception) {
        print("用户名格式错误,请重新输入");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

当输入abc的时候,执行结果:

请输入用户名:abc 用户名格式错误,请重新输入

# 11.6 自定义异常类

除了使用 Kotlin 内置的异常类型外,您还可以自定义异常类,以便更好地描述特定的异常情况。自定义异常类通常继承自 Exception或其子类。建议以Exception结尾,见名知意。

举个栗子:

下面我们首先自定义了两个异常类,然后根据需要抛出这两个异常类的对象,在捕获异常的时候,根据异常类型进行不同的处理。

并在自定义异常类中可以封装自定义的参数。

/**
 * 自定义一个用户名为空的异常
 */
class UsernameEmptyException : Exception("用户名为空")

/**
 * 自定义一个用户名长度不正确的异常
 */
class UsernameLengthException(message: String, val minLength: Int, val maxLength: Int) : Exception(message) {
    override fun toString(): String {
        return "${this.message}, minLength:${this.minLength}, maxLength:${this.maxLength}"
    }
}

fun inputUsername(): String {
    print("请输入用户名:");
    // 读取键盘输入
    val name = readLine()

    if (null == name || name.length < 1) {
        throw UsernameEmptyException(); // 抛出用户名为空的异常
    }

    if (name.length > 16 || name.length < 8) {
        throw UsernameLengthException("用户名格式错误", 8, 16); // 抛出用户名长度异常
    }

    return name;
}

fun main() {
    try {
        var password = inputUsername();
        println("输入的用户名:$password");
    } catch (e: UsernameEmptyException) {
        println("用户名不能为空")
    } catch (e: UsernameLengthException) {
        println("用户名长度不正确")
        println(e)
    } catch (e: Exception) {
        println("用户名格式错误,请重新输入");
    }
}

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
41
42
43
44

在上面的代码中,我们自定义了两个异常,并在代码中根据条件抛出了这两个异常。

然后在异常捕获的时候,可以针对这两个异常进行不同的处理。

# 11.7 先决条件函数

Kotlin 标准库中提供了一些内置函数,可以抛出自带信息的异常,这些便利函数叫做先决条件函数。

先决条件函数用于在执行某个操作之前验证一个条件。如果条件不满足,这些函数将引发 IllegalArgumentExceptionIllegalStateException。这有助于在程序中更早地检测错误,并提供更有意义的错误消息。

以下是 Kotlin 中的主要先决条件函数:

# 1 require

fun main() {
    val a:Int = 3
    val b:Int = 1
    require(b != 0) {"除数是0"}

    val c = a / b
    println(c)
}
1
2
3
4
5
6
7
8

require 函数用于检查传递给它的条件是否为 true。如果条件为 false,则会抛出 IllegalArgumentException 异常,同时可以提供一个可选的错误消息。

所以上面的代码,如果 b 不等于 0,代码正常执行,如果 b 等于 0,则 require 会抛出异常,执行结果如下:

# 2 check

fun validateInput(input: Int) {
    check(input >= 0) { "input 必须大于等于0" }
    // 继续处理输入
}
1
2
3
4

check 函数类似于 require,也用于检查条件。如果条件为 false,则会抛出 IllegalStateException 异常,同时可以提供一个可选的错误消息。

虽然 checkrequire在功能上有一些相似之处,但在某些场景下,使用不同的异常类型有助于提供更具体的错误信息,从而更容易定位和调试问题。

# 3 assert

在 Kotlin 中,assert 函数已被设计为用于测试目的,而不是用于生产代码的错误检查。默认情况下,assert 在生产代码中是禁用的。你需要通过 -ea(或 --enableassertions)标志来启用它。运行代码的时候使用debug模式才会生效。

fun validateInput(input: Int) {
    assert(input >= 0) { "Input 必须大于等于0" }
    // 继续处理输入
}
1
2
3
4

# 4 requireNotNull

fun processNullableInput(input: String?) {
    val nonNullInput = requireNotNull(input) { "input 不能为null" }
    // 继续处理非空输入
}
1
2
3
4

requireNotNull 函数用于检查一个值是否为 null,如果为空,它会抛出 IllegalArgumentException 异常,同时可以提供一个可选的错误消息。如果不为空,则将值返回。

这些先决条件函数是 Kotlin 中用于检查先决条件的常见方式,它们有助于在程序中及早发现错误,并提供有意义的错误消息。在实际应用中,它们通常用于参数验证或其他需要满足特定条件的地方。