5.4.2 数值

原文链接: https://kotlinlang.org/docs/numbers.html

5.4.2 数值

Kotlin 的数值类型表示:

使用数值类型来存储和处理数值数据,例如用于算术运算、计数器、测量以及其他计算。

选择数值类型

在大多数情况下,你可以参考以下规则来确定适合你的任务的数值类型:

  • 整数使用 Int。
  • 超出 Int 范围的整数使用 Long。
  • 小数使用 Double。
  • 当较低精度可以接受或必需时使用 Float。
  • 当 API 或数据格式要求时使用 Byte 和 Short。

提示: Kotlin 还以 Beta 特性形式提供了无符号整数类型。

整数类型

Kotlin 提供四种大小和取值范围各不相同的整数类型:

| 类型 | 大小(位) | 最小值 | 最大值 | | Byte | 8 | -128 | 127 | | Short | 16 | -32768 | 32767 | | Int | 32 | -2,147,483,648 (-231) | 2,147,483,647 (231 - 1) | | Long | 64 | -9,223,372,036,854,775,808 (-263) | 9,223,372,036,854,775,807 (263 - 1) |

声明整数值

Kotlin 支持以下整数值字面量形式:

  • 十进制:123
  • 十六进制:0x0F
  • 二进制:0b00001011

注意: Kotlin 不支持八进制字面量。

要声明一个数值,请显式指定类型:

1
2
3
4
5
6
7
8
9
val one: Int = 1

// Use underscores to improve readability
val oneBillion: Long = 1_000_000_000
val hexBytes: Int = 0x7F_EC_DE_5E
val bytes: Int = 0b01010010_01101001_10010100_10010010

val oneByte: Byte = 1
val oneShort: Short = 1

你也可以加上 L 后缀来声明 Long 值:

1
val oneLong = 1L

当你显式声明数值类型时,编译器会检查该值是否落在该类型的取值范围内:

1
2
3
4
5
// Value fits in Byte
val oneByte: Byte = 1

// Error: the value does not fit in Byte
val tooBig: Byte = 128

当你不指定数值类型时,如果该值落在 Int 范围内,Kotlin 会推断为 Int;否则推断为 Long:

1
2
val million = 1_000_000 // Int
val threeBillion = 3_000_000_000 // Long

如果值可能缺失,请使用可空类型:

1
val maybeAbsent: Int? = null

浮点类型

对于带小数部分的数值,Kotlin 提供了 Float 和 Double。

浮点类型遵循 IEEE 754 标准。Float 对应单精度,Double 对应双精度。

浮点类型在大小和精度上有所不同:

| 类型 | 大小(位) | 有效位 | 指数位 | 十进制位数 | | Float | 32 | 24 | 8 | 6-7 | | Double | 64 | 53 | 11 | 15-16 |

声明浮点值

要声明浮点字面量,请包含小数点(.)或使用指数表示法:

1
2
val pi = 3.14
val avogadro = 6.02214076e23

默认情况下,Kotlin 把浮点字面量推断为 Double。要声明 Float,请加上 f 或 F 后缀:

1
2
val pi = 3.14 // Double
val eFloat = 2.7182817f // Float

注意: 对于精度超出 Float 所能存储范围的 Float 字面量,Kotlin 会进行舍入。

如果值可能缺失,请使用可空类型:

1
val maybeAbsent: Double? = null

算术运算

Kotlin 支持对数值进行标准算术运算:+、-、*、/ 和 %。

使用这些运算符执行常见计算:

1
2
3
4
5
6
fun main() {
    println(1 + 2) // 3
    println(2_500_000_000L - 1L) // 2499999999
    println(3.14 * 2.71) // 8.5094
    println(10.0 / 3) // 3.3333333333333335
}

结果类型取决于操作数的类型。更多内容请参阅混合数值表达式。

提示: 你可以在自定义的数值类中重写这些运算符。更多信息请参阅运算符重载。

整数除法

整数值之间的除法总是返回整数结果。编译器会丢弃小数部分:

1
2
3
4
5
6
7
fun main() {
    val intValue = 5 / 2
    println(intValue) // 2

    val longValue = 5L / 2
    println(longValue) // 2
}

要返回浮点结果,请让至少一个操作数是 Float 或 Double:

1
2
3
4
5
6
7
fun main() {
    val a = 5 / 2.0
    println(a) // 2.5

    val b = 5 / 2.toDouble()
    println(b) // 2.5
}

类型转换

数值类型之间并不是子类型关系。Kotlin 要求显式转换,以避免静默的数据丢失和意外行为。

例如,期望 Double 的函数无法在不转换的情况下接受 Int 或 Float 值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun main() {
    fun printDouble(x: Double) {
        print(x)
    }

    val x = 1.0
    val xInt = 1
    val xFloat = 1.0f
    val one: Double = 1 // Error: initializer type mismatch

    printDouble(x) // OK
    printDouble(xInt) // Error: argument type mismatch
    printDouble(xFloat) // Error: argument type mismatch
}

所有数值类型都支持转换为其他数值类型。要把一个数值转换为另一种类型,请使用显式转换函数:

  • toByte()
  • toShort()
  • toInt()
  • toLong()
  • toFloat()
  • toDouble()

例如,下面的代码把 Int 值转换为 Double:

1
2
3
4
5
6
fun main() {
    val intValue: Int = 1
    val doubleValue = intValue.toDouble()

    println(doubleValue) // 1.0
}

当你把浮点值转换为整数类型时,编译器会丢弃小数部分:

1
2
3
4
5
6
fun main() {
    val d: Double = 1.5
    val l: Long = d.toLong()

    println(l) // 1
}

混合数值表达式

Kotlin 不支持在赋值或函数实参中进行隐式转换。不过,你可以在算术表达式中组合不同的数值类型。在这种情况下,Kotlin 会根据操作数的类型确定结果类型,算术运算符会自动处理转换:

1
2
3
val intNumber: Int = 1
val longNumber: Long = 1000
val result = intNumber + longNumber // 1001, Long

如果你试图把结果赋给更小的类型,编译器会报错:

1
2
3
4
val intNumber: Int = 1
val longNumber: Long = 1000
val result: Int = intNumber + longNumber
// Error: Initializer type mismatch

整数字面量类型

在类型推断过程中,直到上下文确定具体类型之前,Kotlin 会把不带后缀的整数字面量当作一种特殊的整数字面量类型(ILT):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fun List<Any>.log() {
    println(joinToString(" | ") { it::class.simpleName ?: "Unknown" })
}

fun main() {
    listOf(1, 2).log()
    // Int | Int

    listOf(1L, 2L).log()
    // Long | Long

    // Compiler interprets 1 as an ILT and resolves it to Long
    listOf(1, 2L).log()
    // Long | Long

    // .toInt() converts the literal to Int
    listOf(1.toInt(), 2L).log()
    // Int | Long
}

对于 Int 和 Long 值尤其容易忽略这一点,因为它们在运行期的字符串表示相同。要避免这个问题,请指定期望的类型或显式转换值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun List<Any>.log() {
    println(joinToString(" | ") { it::class.simpleName ?: "Unknown" })
}

fun main() {
    val longValues: List<Long> = listOf(1, 2L)
    longValues.log()
    // Long | Long

    val numberValues: List<Number> = listOf(1.toInt(), 2L)
    numberValues.log()
    // Int | Long
}

你也可以使用显式类型来捕获意外的类型推断:

1
2
3
4
fun main() {
    val intValues: List<Int> = listOf(1, 2L)
    // Error: initializer type mismatch
}

提示: 了解更多关于整数字面量类型的内容。

数据溢出

数值类型只能表示其既定范围内的值。

如果运算结果落在该范围之外,就会发生溢出。如果你把值转换为更小的数值类型,转换后的值可能不再保留原来的数值。

即使编译器接受这样的代码,这种行为也可能影响代码的结果。

运算中的溢出

每种整数类型都只能存储其既定范围内的值。当算术运算的结果超出该范围时,就会发生数据溢出:

1
2
3
4
5
fun main(){
    val intNumber: Int = 2147483647
    // Max Int value is 2147483647
    println(intNumber + 1) // -2147483648
}

这里的值发生了回绕,因为它不再能放入 Int。

注意: 当发生整数溢出时,编译器不会自动报错。

取负中的溢出

取负时也可能发生溢出。例如,你无法把 Int.MIN_VALUE 的正数对应值表示为 Int。

1
2
3
4
fun main(){
    val min = Int.MIN_VALUE
    println(-min) // -2147483648
}

收窄转换

当你把值转换为更小的整数类型时,结果可能不再保留原来的数值:

1
2
3
4
5
6
fun main() {
    val large: Int = 130
    val narrowed: Byte = large.toByte()

    println(narrowed) // -126
}

不过,由于浮点类型遵循 IEEE 754 标准,非常大的结果可能变成 Infinity:

1
2
3
fun main() {
    println(Double.MAX_VALUE * 2) // Infinity
}

位运算

Kotlin 为 Int 和 Long 提供了位运算。这些运算由一组中缀函数和 inv() 表示。

1
2
3
4
5
6
fun main() {
    val x = 1

    println(x shl 2) // 4
    println(x and 0x000FF000) // 0
}

位运算包括:

  • shl() —— 有符号左移
  • shr() —— 有符号右移
  • ushr() —— 无符号右移
  • and() —— 按位与
  • or() —— 按位或
  • xor() —— 按位异或
  • inv() —— 按位取反

浮点数比较

在 Kotlin 中,浮点数比较取决于操作数的静态类型。

当操作数的静态类型确定是 Float 或 Double 时,对这些数值及其构成的区间的操作遵循 IEEE 754 浮点运算标准。

然而,在泛型用法中(例如 Any、Comparable<...> 或 Collection<T>),对于静态类型不是浮点数的操作数,行为会有所不同。在这些情况下,Kotlin 使用 Float 和 Double 的 equals() 与 compareTo() 实现。

其结果是:

  • NaN 被认为与自身相等
  • NaN 被认为大于其他任何元素,包括 POSITIVE_INFINITY
  • -0.0 被认为小于 0.0

下面的示例展示了静态类型为浮点数的操作数,与通过泛型类型使用的操作数之间的差异:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun generalizedEquals(a: Any, b: Any): Boolean {
    return a == b
}

fun main() {
    // Operands statically typed as floating-point numbers
    println(Double.NaN == Double.NaN) // false
    println(0.0 == -0.0) // true

    // Operands used through a non-floating-point static type
    println(generalizedEquals(Double.NaN, Double.NaN)) // true
    println(generalizedEquals(0.0, -0.0)) // false
}

JVM 上的数值装箱与缓存

在 JVM 上,非空的数值通常使用基本类型存储,例如 int、long 或 double。不过,当你使用泛型类型或 Int? 这样的可空数值类型时,值会被装箱并以对象形式表示。

JVM 对小数值采用了一种内存优化技术,会缓存它们的装箱表示。因此,值相同的装箱数值可能引用相等。

例如,JVM 会缓存 -128 到 127 范围内的装箱 Integer 值。因此,下面的代码返回 true:

1
2
3
4
5
6
7
fun main() {
    val score: Int = 100
    val savedScore: Int? = score
    val displayedScore: Int? = score

    println(savedScore === displayedScore) // true
}

对于缓存范围之外的值,装箱后的值是彼此独立的对象。在这种情况下,即使它们的值结构相等,它们也不是引用相等的。因此,比较数值时请使用 ==:

1
2
3
4
5
6
7
8
fun main() {
    val score: Int = 10000
    val savedScore: Int? = score
    val displayedScore: Int? = score

    println(savedScore === displayedScore) // false
    println(savedScore == displayedScore) // true
}