数字、布尔值与字符

Kotlin 的基础类型在源码层面看起来都是对象:IntLongBooleanChar 都可以调用成员函数。但这不等于它们在 JVM 上总是对象。Kotlin 编译器会在很多场景下把非空基础值编译成 JVM primitive,以保留性能;在可空、泛型、反射或互操作场景中才可能装箱。

本章覆盖官方 NumbersBooleansCharactersUnsigned integer types 页面,并补充 Java 迁移时最容易踩的点。

数字类型总览

Kotlin 数字分为整数和浮点数:

类型 位数 Java 接近类型 常见用途
Byte 8 byte / Byte 二进制协议、文件格式
Short 16 short / Short 特定协议或旧 API
Int 32 int / Integer 默认整数选择
Long 64 long / Long 大范围整数、时间戳、ID
Float 32 float / Float 低精度浮点、图形/模型 API
Double 64 double / Double 默认小数选择

官方建议可以简化成:

  • 整数默认用 Int
  • 超过 Int 范围用 Long
  • 小数默认用 Double
  • 只有 API、数据格式或性能明确要求时才用 FloatByteShort

Java 对比:Java 里 intInteger 是两套类型;Kotlin 源码通常只写 Int,编译器根据上下文决定是否使用 primitive 或 boxed representation。

整数字面量

Kotlin 支持这些整数字面量:

val decimal = 123
val hex = 0x0F
val binary = 0b00001011

不支持八进制字面量。Java 里历史上 0123 是八进制,这在 Kotlin 中不会成立,避免了不少误读。

可以使用下划线提高可读性:

val oneMillion = 1_000_000
val cardNumber = 1234_5678_9012_3456L
val bytes = 0b01010010_01101001_10010100_10010010

Long 字面量可加 L

val id = 42L

不写类型时:

  • 如果值适合 Int,推断为 Int
  • 如果值超出 Int 但适合 Long,推断为 Long
val million = 1_000_000          // Int
val threeBillion = 3_000_000_000 // Long

显式类型会让编译器检查范围:

val oneByte: Byte = 1
// val tooBig: Byte = 128 // 编译错误

浮点数字面量

小数字面量默认是 Double

val pi = 3.14       // Double
val distance = 1e3  // Double,1000.0

要声明 Float,使用 fF 后缀:

val ratio = 0.75f

Float 只有约 6 到 7 位十进制有效数字,Double 约 15 到 16 位。金融金额、精确十进制、税费计算不要用二进制浮点数,通常应该使用 BigDecimal 或整数分单位建模。

Java 对比:这点和 Java 一样,float / double 都遵循 IEEE 754。Kotlin 没有改变浮点数的底层语义。

整数除法

两个整数相除,结果仍是整数:

val a = 5 / 2
println(a) // 2

要得到小数,至少一个操作数要是 FloatDouble

val b = 5 / 2.0
println(b) // 2.5

val c = 5.toDouble() / 2
println(c) // 2.5

Java 对比:这和 Java 一致。迁移时不要因为 Kotlin 语法更高级就误以为 / 会自动返回小数。

没有隐式数字转换

Kotlin 不允许把 Int 隐式当作 LongDouble 传参:

fun printDouble(value: Double) {
    println(value)
}

val intValue = 1
// printDouble(intValue) // 编译错误

printDouble(intValue.toDouble())

也不能把 Int 直接赋值给 Long

val i = 1
// val l: Long = i // 编译错误
val l: Long = i.toLong()

Java 对比:Java 允许很多 widening primitive conversion,例如 intlongdouble。Kotlin 更严格,要求显式转换,目的是避免 API 调用中悄悄发生精度或语义变化。

所有数字类型都支持显式转换:

val value = 42

value.toByte()
value.toShort()
value.toInt()
value.toLong()
value.toFloat()
value.toDouble()

浮点转整数会丢弃小数部分:

val d = 1.9
println(d.toInt()) // 1

这不是四舍五入。需要四舍五入时使用 roundToInt() 等函数。

混合数字表达式

Kotlin 不支持函数参数和赋值中的隐式转换,但算术表达式可以混合不同数字类型,结果类型由操作数决定:

val intNumber: Int = 1
val longNumber: Long = 1000

val result = intNumber + longNumber // Long

不能把结果塞进更小类型:

// val wrong: Int = intNumber + longNumber // 编译错误

建议公共 API 中避免让调用者猜结果类型。需要明确语义时,先转换再计算:

val total: Long = intNumber.toLong() + longNumber

溢出

整数运算溢出不会自动报错:

val max = Int.MAX_VALUE
println(max + 1) // -2147483648

负数取反也可能溢出:

val min = Int.MIN_VALUE
println(-min) // -2147483648

原因是 Int.MIN_VALUE 的绝对值超出 Int.MAX_VALUE

窄化转换也可能改变值:

val large = 130
val narrowed: Byte = large.toByte()
println(narrowed) // -126

实践建议:

  • 计数、金额、容量等接近上限的值优先评估 Long
  • 安全敏感、金融、库存等场景不要依赖溢出回绕。
  • Java 有 Math.addExact() 等精确检查工具,Kotlin/JVM 可以直接使用。

位运算

Kotlin 没有 Java 风格的 <<>>>>>&|^ 数字位运算符。它提供 IntLong 上的中缀函数:

val x = 1

println(x shl 2)       // 4
println(x and 0xFF)    // 1
println(x or 0x10)     // 17
println(x xor 0x03)    // 2
println(x.inv())       // 按位取反

常用函数:

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

Java 对比:

int result = (x << 2) & 0xFF;

Kotlin:

val result = (x shl 2) and 0xFF

这能避免位运算符和布尔运算符混用,但对底层开发者需要适应。

浮点比较的特殊规则

当操作数静态类型是 FloatDouble 时,比较遵循 IEEE 754:

println(Double.NaN == Double.NaN) // false
println(0.0 == -0.0)              // true

但当值通过非浮点静态类型参与比较,例如 Any、泛型、集合排序,行为会使用 equals() / compareTo() 实现:

fun equalsAsAny(a: Any, b: Any): Boolean = a == b

println(equalsAsAny(Double.NaN, Double.NaN)) // true
println(equalsAsAny(0.0, -0.0))              // false

官方文档特别指出,在泛型场景中:

  • NaN 被认为等于自身。
  • NaN 被认为大于任何其他元素,包括正无穷。
  • -0.0 被认为小于 0.0

实践建议:

  • 不要用浮点数做精确业务等值判断。
  • 排序、去重、Map key 中使用浮点数要格外小心。
  • 如果领域允许误差,写明确的 epsilon 比较函数。

JVM 装箱与数字缓存

Kotlin/JVM 中,非空数字通常编译为 primitive:

val count: Int = 10

但以下场景可能装箱:

  • 可空数字:Int?
  • 泛型:List<Int> 中的元素。
  • Any
  • Java 互操作需要对象类型。

JVM 会缓存一部分小整数的 boxed 对象。以 Integer 为例,通常缓存 -128127

val score: Int = 100
val a: Int? = score
val b: Int? = score

println(a === b) // true,可能来自 JVM 缓存

超出缓存范围:

val score: Int = 10_000
val a: Int? = score
val b: Int? = score

println(a === b) // false
println(a == b)  // true

结论:数字值比较永远用 ==,不要用 ====== 比较引用身份,只在你明确需要对象身份时使用。

Java 对比:Java 中 Integer a = 100; Integer b = 100; a == b 也可能因为缓存返回 true,而大整数返回 false。Kotlin 的 == 会调用结构相等,规避了大部分误用。

Boolean

Boolean 只有两个值:truefalse

val enabled: Boolean = true
val visible = false
val optional: Boolean? = null

Kotlin 不允许把整数当布尔值:

// val enabled: Boolean = 1 // 编译错误

Java 也不允许 int 直接作为 boolean,但 C/C++、JavaScript 等语言允许类似写法。Kotlin 保持严格。

Boolean 运算

逻辑非:

val isOn = true
val isOff = !isOn

逻辑与和或:

val canAccess = isLoggedIn && hasPermission
val shouldShow = isAdmin || isOwner

&&|| 会短路:

fun expensiveCheck(): Boolean {
    println("checked")
    return true
}

val result = false && expensiveCheck() // expensiveCheck 不会执行

如果你确实要两边都执行,可以用 and / or 中缀函数:

val result = false and expensiveCheck() // expensiveCheck 会执行

异或使用 xor

val onlyOne = a xor b

优先级上,官方文档给出的顺序是:

  1. !
  2. xor 等中缀函数
  3. &&
  4. ||

实践建议:复杂布尔表达式主动加括号,不要让读者背优先级。

Boolean? 不是三值逻辑的万能解

Boolean? 可以表达 true、false、null,但不要随意用它表示业务状态:

val approved: Boolean? = null

这可能表示“未审核”“未知”“不适用”“加载失败”。更清晰的做法是 sealed class 或 enum:

enum class ApprovalStatus {
    Pending,
    Approved,
    Rejected,
}

只有当 null 的含义非常明确,例如“配置未提供”,才使用 Boolean?

Char

Char 表示一个 UTF-16 code unit:

val letter: Char = 'a'
val digit = '1'
val symbol = '!'
val space = ' '

字符字面量必须恰好包含一个字符:

// val invalid = 'AB' // 编译错误
// val empty = ''     // 编译错误

Char 不是数字类型:

val c = 'A'
// val code: Int = c // 编译错误
val code: Int = c.code

Java 对比:Java char 本质上是 16 位无符号整数,很多时候可以参与数字运算。Kotlin 明确把 Char 与数字类型分开,需要显式 .code 或转换函数。

Unicode 与 surrogate pair

官方文档强调:Kotlin Char 存储的是 UTF-16 code unit,不一定是完整 Unicode 字符。

BMP 范围内字符可以用一个 Char 表示:

val one = '\u0031'
println(one) // 1

BMP 之外的字符,例如很多 emoji,需要两个 Char 组成 surrogate pair:

val emoji = "😀"
println(emoji.length) // 2
println(emoji[0])     // high surrogate
println(emoji[1])     // low surrogate

所以:

  • String.length 不是用户感知字符数。
  • string[i] 取到的是某个 UTF-16 code unit。
  • 处理 emoji、多语言文本、截断昵称时,不能简单按 Char 切。

Java 对比:Java String.length() 也返回 UTF-16 code unit 数,问题相同。

字符转义

常用转义:

写法 含义
\t Tab
\b Backspace
\n 换行
\r 回车
\' 单引号
\" 双引号
\\ 反斜杠
\$ 美元符号

示例:

val newLine = '\n'
val dollar = '\$'
val backslash = '\\'

Unicode 字符:

val letterA = '\u0041'

字符操作

比较按 Unicode 数值:

println('a' < 'b') // true
println('A' == 'a') // false

检查和大小写转换:

val c = 'A'

println(c.isDigit())      // false
println(c.isUpperCase())  // true
println(c.lowercaseChar()) // a

字符加减整数:

val a = 'a'
println(a + 1) // b
println(a + 2) // c

这按 Unicode 值移动,不是自然语言字母表规则。不要用它处理本地化文本排序或大小写规则。

数字字符转换:

val digit = '7'
println(digit.digitToInt()) // 7

不确定是否为数字时:

val value = c.digitToIntOrNull()

无符号整数

Kotlin 提供无符号整数类型:

类型 位数 范围
UByte 8 0 到 255
UShort 16 0 到 65,535
UInt 32 0 到 4,294,967,295
ULong 64 0 到 18,446,744,073,709,551,615

字面量:

val a = 42u      // UInt
val b = 1UL      // ULong
val c: UByte = 1u

典型用途:

  • 使用完整 bit 范围表示正值,例如颜色 AARRGGBB
  • 二进制协议、文件格式、网络协议。
  • Native API 互操作。

示例:

data class Color(val value: UInt)

val yellow = Color(0xFFCC00CCu)

不要把无符号类型当作“非负业务整数”的默认选择。官方文档明确说明,不建议把它用于集合大小或索引。原因包括:

  • 空集合的 lastIndex-1,有符号整数能自然表达这类错误/边界值。
  • 无符号和有符号整数不是子类型关系,互相转换会增加复杂度。
  • 很多 Java/JVM API 仍使用有符号整数。

无符号数组

每个无符号类型都有数组类型:

  • UByteArray
  • UShortArray
  • UIntArray
  • ULongArray

它们避免装箱,类似 IntArrayint[] 的作用。但官方文档说明,无符号数组及其操作仍是 Beta,需要 @ExperimentalUnsignedTypes opt-in:

@OptIn(ExperimentalUnsignedTypes::class)
fun utf8Bom(): UByteArray =
    ubyteArrayOf(0xEFu, 0xBBu, 0xBFu)

公开 API 中暴露无符号数组要谨慎,因为 Beta 能力可能不兼容变化。

实践建议

  • 默认整数用 Int,默认小数用 Double
  • 需要跨 API、持久化或协议时,显式写清数字类型。
  • 不依赖隐式数字转换,Kotlin 本来也不支持。
  • 数字比较用 ==,不要对 boxed 数字用 ===
  • 复杂布尔表达式加括号。
  • 业务状态不要滥用 Boolean?
  • Char 不是完整 Unicode 字符,处理 emoji 和多语言文本时要按 code point 或文本库处理。
  • 无符号类型用于 bit/protocol/native,不用于普通非负业务值。

官方参考

  • Numbers:https://kotlinlang.org/docs/numbers.html
  • Booleans:https://kotlinlang.org/docs/booleans.html
  • Characters:https://kotlinlang.org/docs/characters.html
  • Unsigned integer types:https://kotlinlang.org/docs/unsigned-integer-types.html