字符串与数组

字符串和数组是 Java 开发者迁移 Kotlin 时最熟悉、也最容易误判的基础类型。Kotlin 的 String 仍然是不可变字符串,JVM 上仍使用 UTF-16;Kotlin 的 Array<T> 看似像 Java T[],但泛型不变、内容相等、primitive array、vararg 传参都和 Java 有明显差异。

本章覆盖官方 StringsArrays 页面,并补充 Java 对比。

String 基础

Kotlin 字符串类型是 String

val language = "Kotlin"

字符串元素是 Char,可以用索引访问:

val text = "abcd"

println(text[0]) // a
println(text[3]) // d

也可以遍历:

for (char in text) {
    println(char)
}

注意:索引访问得到的是 UTF-16 code unit 对应的 Char,不一定是用户感知的完整字符。emoji 等 BMP 之外字符可能占两个 Char

String 不可变

Kotlin 字符串不可变:

val text = "abcd"
val upper = text.uppercase()

println(upper) // ABCD
println(text)  // abcd

所有转换函数都会返回新字符串,不会修改原对象。

Java 对比:Java String 也不可变。需要大量拼接时,Java 常用 StringBuilder;Kotlin 也可以直接使用 StringBuilderbuildString {}

val result = buildString {
    append("Hello")
    append(", ")
    append("Kotlin")
}

字符串拼接

可以用 +

val message = "Hello, " + "Kotlin"

当第一个元素是字符串时,可以拼接其他类型:

val value = "answer = " + 42

但多数场景更推荐字符串模板:

val answer = 42
val message = "answer = $answer"

复杂表达式用 ${...}

val name = "Ada"
println("length = ${name.length}")

Java 对比:

String message = "answer = " + answer;

Kotlin 模板更适合嵌入变量和简单表达式,但复杂业务逻辑不要塞进模板里。

转义字符串

普通字符串用双引号,支持转义:

val text = "Hello,\nKotlin"
val quote = "She said \"Hi\""
val path = "C:\\Users\\Ada"

常见转义规则与字符转义一致,例如 \n\t\\\"\$

如果要在普通字符串中输出 $

val price = "\$9.99"

多行字符串

多行字符串使用三个双引号:

val sql = """
    SELECT id, name
    FROM users
    WHERE active = true
"""

多行字符串不使用反斜杠转义,可以包含换行和普通文本。为了去掉缩进,常用 trimIndent()trimMargin()

val text = """
    |Tell me and I forget.
    |Teach me and I remember.
    |Involve me and I learn.
""".trimMargin()

默认 margin prefix 是 |,也可以自定义:

val text = """
    >line one
    >line two
""".trimMargin(">")

Java 对比:Java 15+ 有 text block,但 Kotlin 多行字符串与模板表达式结合更早、更自然。

多行字符串中的美元符号

多行字符串不支持反斜杠转义,因此不能写 \$。传统做法是:

val template = """
    Price: ${'$'}9.99
"""

这在写 JSON Schema、Shell、模板语言、正则说明时很烦,因为 $ 很常见。

Multi-dollar string interpolation

官方字符串页面加入了 multi-dollar string interpolation。它允许你指定几个连续 $ 才触发插值。

例如用 $$"""...""" 时,只有两个连续美元符号才触发 Kotlin 插值,单个 $ 保持字面量:

val name = "product"

val schema = $$"""
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "$$name"
}
"""

这里:

  • "$schema" 中的单个 $ 保留为普通字符。
  • "$$name" 触发 Kotlin 插值,输出 product

三个美元符号也可以:

val productName = "carrot"

val data = $$$"""
{
  "currency": "$",
  "serviceField": "$$service",
  "product": "$$$productName"
}
"""

此时 $$$ 是字面量,$$$productName 才插值。

因为该能力是较新的语言特性,团队使用前应确认 Kotlin 版本、语言版本和 IDE 支持。

String.format

String.format() 只在 Kotlin/JVM 可用:

val padded = String.format("%07d", 31416)
println(padded) // 0031416

val pi = String.format("%+.4f", 3.141592)
println(pi) // +3.1416

它适合:

  • 固定位数。
  • 小数精度。
  • 对齐、宽度、补零。
  • 需要复用 Java Formatter 格式的场景。

普通变量插入优先字符串模板:

val name = "Ada"
println("Hello, $name")

格式化本地化文本时,要注意 locale。String.format() 使用的格式规则来自 Java Formatter,参数数量和位置不匹配时容易运行时报错。

字符串常用判断

Kotlin 标准库提供很多比 Java 更直接的工具:

val text = "  Kotlin  "

println(text.isEmpty())
println(text.isBlank())
println(text.trim())
println(text.startsWith("  K"))
println(text.contains("otl"))

可空字符串:

fun normalize(input: String?): String =
    if (input.isNullOrBlank()) "unknown" else input.trim()

Java 对比:Java 11 有 isBlank()strip() 等,但可空处理仍要自己写判断。Kotlin 的 isNullOrBlank() 是迁移中非常常用的工具。

Array 什么时候使用

官方数组页面明确建议:普通业务代码优先使用集合,只有低层或特殊要求时使用数组。

数组适合:

  • 性能敏感的低层代码。
  • 与 Java API、JNI、二进制协议互操作。
  • 固定大小数据结构。
  • primitive array 避免装箱。

集合更适合:

  • 业务列表、集合、映射。
  • 需要只读接口表达意图。
  • 需要增删元素。
  • 需要结构相等比较。

Java 开发者容易把数组当作默认容器,但 Kotlin 中大多数业务场景用 List / MutableList 更合适。

创建数组

使用 arrayOf()

val numbers = arrayOf(1, 2, 3)
println(numbers.joinToString()) // 1, 2, 3

创建可空元素数组:

val values: Array<Int?> = arrayOfNulls(3)
println(values.joinToString()) // null, null, null

空数组:

val names = emptyArray<String>()
val other: Array<String> = emptyArray()

使用构造函数按索引初始化:

val squares = Array(5) { index -> index * index }
println(squares.joinToString()) // 0, 1, 4, 9, 16

Java 对比:

int[] squares = new int[5];
for (int i = 0; i < squares.length; i++) {
    squares[i] = i * i;
}

Kotlin 的 Array(size) { index -> ... } 更适合初始化有规律的数组。

数组固定长度但元素可变

数组大小固定,但元素可修改:

val values = arrayOf(1, 2, 3)
values[0] = 10

println(values[0]) // 10

val 只表示变量引用不能重新指向别的数组,不表示数组内容不可变:

val values = arrayOf(1, 2, 3)
values[0] = 99        // 可以
// values = arrayOf(4) // 不可以

这和 Java final int[] values 一样。

数组添加元素的成本

数组固定大小。使用 += 看起来像添加元素,实际上会创建新数组并复制旧元素:

var rivers = arrayOf("Nile", "Amazon", "Yangtze")
rivers += "Mississippi"

如果需要频繁增删,使用 MutableList

val rivers = mutableListOf("Nile", "Amazon", "Yangtze")
rivers.add("Mississippi")

嵌套数组

二维数组:

val matrix = Array(2) { Array(3) { 0 } }
matrix[0][1] = 42

println(matrix.contentDeepToString())

嵌套数组不要求每一层长度相同:

val ragged = arrayOf(
    arrayOf(1, 2),
    arrayOf(3, 4, 5),
)

Java 对比:这类似 Java 的数组数组,不是连续内存中的矩阵。如果需要高性能数值矩阵,应该考虑专门的数据结构。

数组不变性

Kotlin 的 Array<T> 是不变的。不能把 Array<String> 赋给 Array<Any>

val strings: Array<String> = arrayOf("a", "b")
// val anyArray: Array<Any> = strings // 编译错误

原因是如果允许这样做,就可能写入非字符串:

// anyArray[0] = 42

Java 数组是协变的,String[] 可以赋给 Object[],但写入错误类型会运行时报 ArrayStoreException。Kotlin 在编译期阻止这类问题。

如果只读,可以用投影:

fun printAll(values: Array<out Any>) {
    values.forEach(::println)
}

printAll(strings)

Array<out Any> 表示这个函数只从数组读,不往里写任意 Any

vararg 与展开运算符

Kotlin 函数可以使用 vararg

fun printAll(vararg values: String) {
    for (value in values) {
        print(value)
    }
}

printAll("a", "b", "c")

如果已有数组,传给 vararg 需要展开运算符 *

val values = arrayOf("b", "c")
printAll("a", *values)

Java 对比:

void printAll(String... values) {}

String[] values = {"b", "c"};
printAll("a", values); // Java 语法和 Kotlin 不同

Kotlin 用 * 明确表示“把数组展开成多个参数”,避免数组作为单个参数和展开参数混淆。

数组相等

数组的 == 不比较内容,而是比较数组对象身份:

val a = arrayOf(1, 2, 3)
val b = arrayOf(1, 2, 3)

println(a == b) // false

比较内容:

println(a.contentEquals(b)) // true

嵌套数组:

val x = arrayOf(arrayOf(1, 2))
val y = arrayOf(arrayOf(1, 2))

println(x.contentDeepEquals(y)) // true

打印内容:

println(a.contentToString())
println(x.contentDeepToString())

Java 对比:Java 数组 equals() 也比较对象身份,内容比较要用 Arrays.equals() / Arrays.deepEquals()。Kotlin 的坑本质相同,只是函数名更直接。

集合则不同:

println(listOf(1, 2, 3) == listOf(1, 2, 3)) // true

这也是官方建议业务代码优先集合的原因之一。

数组转换

数组转集合:

val values = arrayOf("a", "b", "c", "c")

val list = values.toList()
val set = values.toSet()

Array<Pair<K, V>> 可以转 Map

val entries = arrayOf(
    "apple" to 120,
    "banana" to 150,
    "apple" to 140,
)

println(entries.toMap()) // {apple=140, banana=150}

如果 key 重复,后面的值覆盖前面的值。

集合转数组:

val names = listOf("Ada", "Bob")
val array = names.toTypedArray()

Primitive arrays

如果用 Array<Int>,JVM 上元素会装箱为 Integer。性能敏感时使用 primitive array:

Kotlin Java
BooleanArray boolean[]
ByteArray byte[]
CharArray char[]
ShortArray short[]
IntArray int[]
LongArray long[]
FloatArray float[]
DoubleArray double[]

创建:

val values = IntArray(5)
println(values.joinToString()) // 0, 0, 0, 0, 0

按索引初始化:

val squares = IntArray(5) { index -> index * index }
println(squares.joinToString()) // 0, 1, 4, 9, 16

使用工厂函数:

val bytes = byteArrayOf(1, 2, 3)
val chars = charArrayOf('a', 'b')

Primitive arrays 和 Array<T> 没有继承关系,但提供相似的函数和属性。

Primitive array 与 object array 转换

IntArrayArray<Int>

val primitive: IntArray = intArrayOf(1, 2, 3)
val boxed: Array<Int> = primitive.toTypedArray()

Array<Int>IntArray

val boxed: Array<Int> = arrayOf(1, 2, 3)
val primitive: IntArray = boxed.toIntArray()

这些转换会分配新数组,不能在性能敏感路径里频繁做。

ByteArray 与二进制数据

ByteArray 是 Kotlin/JVM 中处理二进制数据最常见的类型:

val bytes: ByteArray = "Kotlin".encodeToByteArray()
val text: String = bytes.decodeToString()

Java 互操作:

fun readAll(input: java.io.InputStream): ByteArray =
    input.readBytes()

注意 Kotlin Byte 是有符号的,范围 -128..127。处理协议中的无符号字节时,常见做法是:

val byte: Byte = (-1).toByte()
val unsigned: Int = byte.toInt() and 0xFF
println(unsigned) // 255

也可以在适合场景使用 UByte / UByteArray,但无符号数组仍需要 opt-in,公开 API 中要谨慎。

CharArray 与 String

CharArray 可用于需要可变字符缓冲区的场景:

val chars = charArrayOf('K', 'o', 't', 'l', 'i', 'n')
val text = chars.concatToString()

字符串转字符数组:

val chars = "Kotlin".toCharArray()

安全敏感场景中,密码等信息有时用 CharArray 而不是 String,因为 String 不可变且生命周期不可控。但这只有在你能控制整个链路清理内存时才有意义;一旦传给日志、异常或不可控 API,优势就会消失。

数组实践建议

  • 业务列表默认用 List / MutableList
  • 只有低层、性能、互操作、固定大小需求明确时使用数组。
  • 数组内容比较用 contentEquals() / contentDeepEquals()
  • 大量数值数据用 IntArrayDoubleArray 等 primitive arrays。
  • 不要在循环里反复 array += value
  • Java varargs 互操作时注意 *array 展开。
  • 对外 API 尽量少暴露可变数组;必要时做 defensive copy。

官方参考

  • Strings:https://kotlinlang.org/docs/strings.html
  • Arrays:https://kotlinlang.org/docs/arrays.html
  • Characters:https://kotlinlang.org/docs/characters.html
  • Java to Kotlin strings guide:https://kotlinlang.org/docs/java-to-kotlin-idioms-strings.html
  • Java to Kotlin collections guide:https://kotlinlang.org/docs/java-to-kotlin-collections-guide.html