Java 到 Kotlin:集合与字符串迁移

集合和字符串是 Java 迁移 Kotlin 时最常改写的代码。Kotlin 标准库提供了更直接的 API,但迁移时不要只追求“短”,还要保持语义清楚、空安全明确、性能可接受。

集合:只读接口与可变接口

Java 的 List 同时包含读取和修改方法:

List<String> names = new ArrayList<>();
names.add("Ada");

Kotlin 区分只读接口和可变接口:

val names: List<String> = listOf("Ada")
val mutableNames: MutableList<String> = mutableListOf("Ada")

只读 List 不能调用 add(),但它不保证底层对象绝对不可变。对外暴露 API 时,List<T> 表达“调用方不应修改”,MutableList<T> 表达“调用方可以修改”。

创建集合

Java:

List<String> names = List.of("Ada", "Grace");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Ada", 36);

Kotlin:

val names = listOf("Ada", "Grace")
val numbers = setOf(1, 2, 3)
val ages = mapOf("Ada" to 36)

可变版本:

val names = mutableListOf("Ada")
val numbers = mutableSetOf(1, 2)
val ages = mutableMapOf("Ada" to 36)

遍历集合

Java:

for (String name : names) {
    System.out.println(name);
}

Kotlin:

for (name in names) {
    println(name)
}

需要索引:

for ((index, name) in names.withIndex()) {
    println("$index: $name")
}

过滤、映射与排序

Java Stream:

List<String> result = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList();

Kotlin:

val result = names
    .filter { it.length > 3 }
    .map { it.uppercase() }
    .sorted()

Kotlin 集合操作默认会产生中间集合。大量数据或长链条处理时,可以使用 asSequence() 延迟计算:

val result = names.asSequence()
    .filter { it.length > 3 }
    .map { it.uppercase() }
    .sorted()
    .toList()

查找元素

Java:

Optional<String> first = names.stream()
    .filter(name -> name.startsWith("A"))
    .findFirst();

Kotlin 常用可空值表达“可能没有”:

val first: String? = names.firstOrNull { it.startsWith("A") }

如果你确信一定存在:

val first = names.first { it.startsWith("A") }

找不到时会抛 NoSuchElementException。业务上可能不存在时,优先使用 firstOrNull()

分组与关联

val usersByRole: Map<String, List<User>> =
    users.groupBy { it.role }

val userById: Map<Long, User> =
    users.associateBy { it.id }

这类操作在 Java Stream 中也能实现,但 Kotlin 标准库命名更贴近集合领域。

Map 访问

val age: Int? = ages["Ada"]

map[key] 返回可空值,因为 key 可能不存在。需要默认值:

val age = ages["Ada"] ?: 0

可变 Map 更新:

val ages = mutableMapOf("Ada" to 36)
ages["Grace"] = 85

字符串模板

Java:

String message = "Hello, " + name + "!";

Kotlin:

val message = "Hello, $name!"

表达式:

val message = "Name length: ${name.length}"

简单变量用 $name,复杂表达式用 ${...}

多行字符串

val sql = """
    SELECT id, name
    FROM users
    WHERE active = true
""".trimIndent()

这比 Java 早期字符串拼接更清晰。Java 15+ 也有 text block,但 Kotlin 多行字符串和字符串模板结合得更自然。

字符串判空

Kotlin 标准库提供常用函数:

text.isEmpty()
text.isBlank()
text.isNullOrEmpty()
text.isNullOrBlank()

区别:

  • empty:长度为 0。
  • blank:全是空白字符也算空白。
  • nullOrEmpty/nullOrBlank:接收可空字符串。

示例:

fun normalize(input: String?): String =
    input.takeUnless { it.isNullOrBlank() } ?: "默认值"

字符串转换数字

Java:

int value = Integer.parseInt(input);

Kotlin:

val value = input.toInt()

可能失败时:

val value: Int? = input.toIntOrNull()

toIntOrNull() 避免用异常表达正常的解析失败,特别适合用户输入、配置、CSV、命令行参数等场景。

正则与替换

val digits = Regex("\\d+")
val found = digits.findAll("a1 b22 c333")
    .map { it.value }
    .toList()

简单替换:

val normalized = text.replace("Java", "Kotlin")

复杂匹配时用 Regex,简单字面替换时用 replace 即可。

迁移建议

  • Java Stream 迁移到 Kotlin 集合链时,先保持语义一致,再考虑 Sequence 性能。
  • 可能不存在的集合结果用 firstOrNull()singleOrNull()getOrNull()
  • 不要把 Java Optional 机械翻译成 Kotlin Optional;Kotlin 通常用可空类型。
  • 字符串拼接优先用模板。
  • 用户输入转换数字时优先用 toIntOrNull() 等安全函数。
  • 对外 API 区分 ListMutableList,表达调用方是否允许修改。

参考

  • 官方集合迁移指南:https://kotlinlang.org/docs/java-to-kotlin-collections-guide.html
  • 官方字符串迁移指南:https://kotlinlang.org/docs/java-to-kotlin-idioms-strings.html