在 Java 中,如果定义的函数没有返回值,就需要用 void 来修饰,即 void 不可以省略。这也在 Java 中不能说函数调用皆是表达式的原因,因为有些方法不具有返回值和类型信息,就不能算作是表达式。
Java 在语言层设计一个 Void 类。java.lang.Void 类类似于 java.lang.Integer,Integer 是为了对基本类型 int 的实例进行装箱操作。Void 的设计则是为了对应 void。由于 void 表示没有返回值,所以,Void 并不能具有实例,它继承自 Object。
在 Kotlin 中,函数在所有的情况下都有返回值类型,所以 Kotlin 引入了 Unit 来代替 Java 中的 void 关键字。
Unit 和 int 一样,都是一种类型,然而,Unit 不代表任何信息。那么,Kotlin 为什么要引入 Unit 呢?很大一个原因是函数式编程。
在 Kotlin 中,对象或者函数都有类型,如果方法的返回类型是 Unit 时,可以省略。
对比:
和 Object 作为 Java 类层级结构的根差不多,Any 类型是 Kotlin 所有非空类型的超类型(非空类型的根)。
但是在 Java 中,Object 只是所有引用类型的超类型(引用类型的根),而基本数据类型并不是类层级结构的一部分。这意味着当我们需要 Object 的时候,不得不使用像 java.lang.Integer 这样的包装类型来表示基本数据类型的值。
而在 Kotlin 中,Any 是所有类型的超类型(所有类型的根),包括像 Int 这样的基本数据类型。
和 Java 一样,把基本数据类型的值赋给 Any 类型的变量时会自动装箱:
val answer: Any = 42 // Any 是引用类型,所以值 42 会被装箱
注意 Any 是非空类型,所以 Any 类型的变量不可以持有 null 值。在 Kotlin 中如果我们需要可以持有任何可能值的变量,包括 null 在内,必须使用 Any? 类型。
在底层,Any 类型对应 java.lang.Object。Kotlin 把 Java 方法参数和返回类型中用到的 Object 类型看作 Any。当 Kotlin 函数使用 Any 时,它会被编译成 Java 字节码中的 Object。
所有的 Kotlin 类中都包含:toString、equals 和 hashCode。这些方法都继承自 Any。Any 并不能使用其他 java.lang.Object 的方法(比如 wait 和 notify),但是可以通过手动把值转换成 java.lang.Object 来调用这些方法。
对于某些 Kotlin 函数来说,“返回类型”的概念没有任何意义,因为它们不会成功地结束。 例如,许多测试库都有一个叫做 fail 的函数,它通过抛出带有特定消息的一场来让当前测试失败。一个包含无限循环的函数也永远不会成功地结束。
当调用这样的函数代码时,知道函数永远不会正常终止是很有帮助的。Kotlin 使用一种特殊的返回类型 Nothing 来表示:
fun main() {
fail("Error occurred")
}
fun fail(message: String): Nothing {
throw java.lang.IllegalStateException(message)
}
// Exception in thread "main" java.lang.IllegalStateException: Error occurred
// at com.ixuea.test.TestKt.fail(Test.kt:158)
// at com.ixuea.test.TestKt.main(Test.kt:152)
// at com.ixuea.test.TestKt.main(Test.kt)
Nothing 类型没有任何值,只有被当作函数返回值使用,或者被当作泛型函数返回值的类型参数使用才会有意义。在其他所有情况下,声明一个不能存储任何值的变量没有任何意义。
Nothing 是没有任何实例的类型,Nothing 类型的表达式不会产生任何值。需要注意的是,任何返回值为 Nothing 的表达式之后的语句都是无法执行的。这里有点像 return 或者 break 的作用。在 Kotlin 中 return、throw 等,返回值都是 Nothing。
注意,返回 Nothing 的函数可以放在 Elvis 运算符的右边来做先决条件检查:
val address = company.address ?: fail("No address")
println(address.city)
上面这个例子展示了在类型系统中拥有 Nothing 为什么极其有用。编译器知道这种返回类型的函数从不正常终止,然后在分析调用这个函数的代码时利用这个信息。在上面的例子中,编译器会把 address 的类型推断成非空,因为它为 null 时的分支处理会始终抛出异常。