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

223 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<String>` |
> | `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)