字符串与数组¶
字符串和数组是 Java 开发者迁移 Kotlin 时最熟悉、也最容易误判的基础类型。Kotlin 的 String 仍然是不可变字符串,JVM 上仍使用 UTF-16;Kotlin 的 Array<T> 看似像 Java T[],但泛型不变、内容相等、primitive array、vararg 传参都和 Java 有明显差异。
本章覆盖官方 Strings 和 Arrays 页面,并补充 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 也可以直接使用 StringBuilder 或 buildString {}。
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 转换¶
IntArray 转 Array<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()。 - 大量数值数据用
IntArray、DoubleArray等 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