Files
obsidian/Kotlin/Синтаксис и типы/Работа с данными/Строки.md
T

10 KiB
Raw Blame History

title, description, tags
title description tags
Строки String как неизменяемый тип, полезные методы, StringBuilder и buildString
kotlin
stdlib

String - неизменяемый тип. Любая «модификация» создаёт новую строку.

val s = "Kotlin"
println(s.length)       // 6
println(s[0])           // K — доступ по индексу, как у массива
println(s.uppercase())  // KOTLIN
println(s)              // Kotlin — исходная строка не изменилась

На JVM это тот же java.lang.String, со всем его поведением: пул строк, интернирование, представление в LATIN-1 или UTF-16.

Сравнение

val a = "Kotlin"
val b = StringBuilder("Kotlin").toString()
println(a == b)       // true  — сравнение содержимого
println(a === b)      // false — разные объекты
println(a.equals("KOTLIN", ignoreCase = true))   // true

== вызывает equals(), поэтому работает правильно всегда. Это главное практическое отличие от Java, где == на строках сравнивает ссылки.

Для сортировки и порядка - compareTo:

println("abc" < "abd")                 // true
println("a".compareTo("b"))            // -1
println(listOf("b", "a", "c").sorted()) // [a, b, c]

Итерация

Строка - последовательность Char:

for (c in "abc") print("$c.")          // a.b.c.
println("abc".toList())                // [a, b, c]
println("hello".count { it == 'l' })   // 2
println("hello".filter { it != 'l' })  // heo
println("abc".map { it.uppercaseChar() }) // [A, B, C]
println("abc".reversed())              // cba

Работают все операции над коллекциями: filter, map, any, count. Только filter и reversed возвращают String, а map - List.

Поиск и проверки

val s = "Hello, Kotlin"
println(s.contains("Kotlin"))      // true
println("Kotlin" in s)             // true — то же самое
println(s.startsWith("Hello"))     // true
println(s.endsWith("lin"))         // true
println(s.indexOf("o"))            // 4 — первое вхождение, -1 если нет
println(s.lastIndexOf("o"))        // 8
println(s.isEmpty())               // false — длина 0
println("   ".isBlank())           // true — пусто или только пробелы
println(s.isNotEmpty())            // true

Для nullable-строк есть безопасные версии, их можно звать без ?.:

val n: String? = null
println(n.isNullOrEmpty())   // true
println(n.isNullOrBlank())   // true
println(n.orEmpty())         // "" — пустая строка вместо null

Извлечение частей

val s = "Hello, Kotlin"
println(s.substring(7))            // Kotlin
println(s.substring(0, 5))         // Hello — [0, 5)
println(s.take(5))                 // Hello — первые 5
println(s.takeLast(6))             // Kotlin
println(s.drop(7))                 // Kotlin — отбросить первые 7
println(s.dropLast(7))             // Hello
println(s.first())                 // H
println(s.last())                  // n
println(s.getOrNull(100))          // null — вместо исключения

take и drop безопасны: если запросить больше длины, вернётся вся строка, а не исключение. substring за границами падает.

println("abc".take(100))       // abc
// println("abc".substring(100)) // StringIndexOutOfBoundsException

Изменение

Функции capitalize() и decapitalize() устарели и удалены. Замена пишется так:

fun String.capitalize2() =
    replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }

fun main() { println("hello".capitalize2()) }   // Hello

Остальные методы возвращают новую строку:

fun String.capitalize2() =
    replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }

val s = "  Hello, Kotlin  "
println(s.trim())                      // "Hello, Kotlin" — с обеих сторон
println(s.trimStart())                 // "Hello, Kotlin  "
println(s.trimEnd())                   // "  Hello, Kotlin"
println("Hello".replace("l", "L"))     // HeLLo — все вхождения
println("Hello".replaceFirst("l", "L")) // HeLlo
println("Hello".lowercase())           // hello
println("hello".uppercase())           // HELLO
println("hello".capitalize2())         // Hello
println("ab".repeat(3))                // ababab
println("5".padStart(3, '0'))          // 005
println("5".padEnd(3))                 // "5  "

Разбиение и сборка

println("a,b,c".split(","))                 // [a, b, c]
println("a1b2c".split("1", "2"))            // [a, b, c] — несколько разделителей
println("a,b,c".split(",", limit = 2))      // [a, b,c]
println("a\nb".lines())                     // [a, b] — по переводам строк
println(listOf("a","b").joinToString("-"))  // a-b
println(listOf(1,2,3).joinToString(
    separator = ", ", prefix = "[", postfix = "]"))   // [1, 2, 3]
println("hello".chunked(2))                 // [he, ll, o]

joinToString - основной способ собрать строку из коллекции, у него есть и transform:

println(listOf(1, 2).joinToString { "#$it" })   // #1, #2

Преобразование в число

println("42".toInt())           // 42
println("42".toIntOrNull())     // 42
println("abc".toIntOrNull())    // null — не бросает исключение
println("3.14".toDouble())      // 3.14
println("ff".toInt(16))         // 255 — с основанием
// println("abc".toInt())       // NumberFormatException

В коде, который разбирает внешние данные, используйте только версии *OrNull.

StringBuilder и buildString

Для сборки строки в цикле конкатенация создаёт мусор - на каждой итерации новый объект. StringBuilder меняет один буфер:

val sb = StringBuilder()
for (i in 1..3) sb.append(i).append(",")
println(sb.toString())        // 1,2,3,

Идиоматичнее - buildString, он сам создаёт билдер и возвращает результат:

val s = buildString {
    append("Привет")
    append(", ")
    append("Kotlin")
    appendLine("!")
    append("Строка 2")
}
println(s)

Полезные методы билдера: append, appendLine, insert(i, x), deleteAt(i), setLength(n), reverse(), clear().

Многострочные строки

val json = """
    {"name": "Kotlin"}
""".trimIndent()

Подробнее: Строковые шаблоны и вывод

[!info] Отличия от Java

Java Kotlin
s.equals(t) s == t
s == t (ссылки) s === t
s.charAt(0) s[0]
s.isEmpty() s.isEmpty(), плюс isBlank(), isNullOrBlank()
s.toUpperCase() s.uppercase()
new StringBuilder() + toString() buildString { }
String.join(",", list) list.joinToString(",")
Integer.parseInt(s) s.toInt() / s.toIntOrNull()
s.split(",") → String[] → List<String>
s.substring(100) падает s.take(100) безопасен

Главное приобретение - *OrNull-функции и безопасные take/drop вместо исключений на каждой опечатке во входных данных.

uppercase() вместо toUpperCase() - не просто переименование: старая версия Java зависела от системной локали, из-за чего "i".toUpperCase() в турецкой локали давало İ. Kotlin по умолчанию использует Locale.ROOT.

Сравнить: Строки в Java

[!warning] Подводные камни length считает кодовые единицы UTF-16, а не символы. Эмодзи и редкие иероглифы занимают две:

println("😀".length)              // 2, а не 1
println("😀".count())             // 2 — то же самое
println("😀".codePointCount(0, "😀".length))   // 1 — настоящее число символов

Разрезать такую строку по индексу - значит получить битые символы.

split с пустым разделителем и пустой строкой дают неожиданный результат:

println("".split(","))        // [] — один пустой элемент, размер 1
println("".split(",").size)   // 1
println("a,,b".split(","))    // [a, , b] — пустой элемент сохраняется

trimIndent не обрезает пробелы внутри строк - только общий отступ.

replace принимает строку, а не regex - в отличие от Java:

println("a.b".replace(".", "-"))          // a-b — точка как символ
println("a.b".replace(Regex("."), "-"))   // --- — regex

В Java String.replaceAll работает с regex, и это частый источник путаницы при переходе.

Строка в when сравнивается по значению, но полнота не проверяется - для String всегда нужен else.

Дополнительно: Char Далее: Char