--- title: Строки description: String как неизменяемый тип, полезные методы, StringBuilder и buildString tags: - kotlin - stdlib --- `String` - неизменяемый тип. Любая «модификация» создаёт новую строку. ```kotlin 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. ## Сравнение ```kotlin 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`: ```kotlin println("abc" < "abd") // true println("a".compareTo("b")) // -1 println(listOf("b", "a", "c").sorted()) // [a, b, c] ``` ## Итерация Строка - последовательность `Char`: ```kotlin 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`. ## Поиск и проверки ```kotlin 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-строк есть безопасные версии, их можно звать без `?.`: ```kotlin val n: String? = null println(n.isNullOrEmpty()) // true println(n.isNullOrBlank()) // true println(n.orEmpty()) // "" — пустая строка вместо null ``` ## Извлечение частей ```kotlin 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` за границами падает. ```kotlin println("abc".take(100)) // abc // println("abc".substring(100)) // StringIndexOutOfBoundsException ``` ## Изменение Функции `capitalize()` и `decapitalize()` устарели и удалены. Замена пишется так: ```kotlin fun String.capitalize2() = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } fun main() { println("hello".capitalize2()) } // Hello ``` Остальные методы возвращают новую строку: ```kotlin 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 " ``` ## Разбиение и сборка ```kotlin 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`: ```kotlin println(listOf(1, 2).joinToString { "#$it" }) // #1, #2 ``` ## Преобразование в число ```kotlin 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` меняет один буфер: ```kotlin val sb = StringBuilder() for (i in 1..3) sb.append(i).append(",") println(sb.toString()) // 1,2,3, ``` Идиоматичнее - `buildString`, он сам создаёт билдер и возвращает результат: ```kotlin val s = buildString { append("Привет") append(", ") append("Kotlin") appendLine("!") append("Строка 2") } println(s) ``` Полезные методы билдера: `append`, `appendLine`, `insert(i, x)`, `deleteAt(i)`, `setLength(n)`, `reverse()`, `clear()`. ## Многострочные строки ```kotlin val json = """ {"name": "Kotlin"} """.trimIndent() ``` Подробнее: [Строковые шаблоны и вывод](../Базовый%20синтаксис/Строковые%20шаблоны%20и%20вывод.md) > [!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` | > | `s.substring(100)` падает | `s.take(100)` безопасен | > > Главное приобретение - `*OrNull`-функции и безопасные `take`/`drop` вместо исключений на каждой опечатке во входных данных. > > `uppercase()` вместо `toUpperCase()` - не просто переименование: старая версия Java зависела от системной локали, из-за чего `"i".toUpperCase()` в турецкой локали давало `İ`. Kotlin по умолчанию использует `Locale.ROOT`. > > Сравнить: [Строки в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Строки.md) > [!warning] Подводные камни > **`length` считает кодовые единицы UTF-16, а не символы.** Эмодзи и редкие иероглифы занимают две: > ```kotlin > println("😀".length) // 2, а не 1 > println("😀".count()) // 2 — то же самое > println("😀".codePointCount(0, "😀".length)) // 1 — настоящее число символов > ``` > Разрезать такую строку по индексу - значит получить битые символы. > > **`split` с пустым разделителем и пустой строкой дают неожиданный результат:** > ```kotlin > println("".split(",")) // [] — один пустой элемент, размер 1 > println("".split(",").size) // 1 > println("a,,b".split(",")) // [a, , b] — пустой элемент сохраняется > ``` > > **`trimIndent` не обрезает пробелы внутри строк** - только общий отступ. > > **`replace` принимает строку, а не regex** - в отличие от Java: > ```kotlin > println("a.b".replace(".", "-")) // a-b — точка как символ > println("a.b".replace(Regex("."), "-")) // --- — regex > ``` > В Java `String.replaceAll` работает с regex, и это частый источник путаницы при переходе. > > **Строка в `when` сравнивается по значению, но полнота не проверяется** - для `String` всегда нужен `else`. Дополнительно: [Char](Char.md) Далее: [Char](Char.md)