Всем привет, меня зовут Сергей Прощаев. Я техлид и руководитель направления Java | Kotlin разработки в FinTech, а ещё преподаю на курсах разработки и архитектуры в OTUS. Мы продолжаем серию «Kotlin для новичков», и сегодня у нас тема, без которой не обходится ни одно реальное приложение: строки и коллекции.
В прошлых статьях мы разобрали переменные, условия, циклы и функции. Теперь наша задача — научиться работать с наборами данных. Клиенты, транзакции, логи, ответы API — всё это списки, массивы и строки. Причём Kotlin настолько дружелюбен к этим сущностям, что после Java чувствуешь себя так, будто пересел со старого грузовика на спорткар.
Мы разберём только самое важное, что нужно знать начинающему разработчику: как резать и форматировать строки, чем List отличается от MutableList, как итерироваться по массивам и зачем вообще нужны enum. И конечно, подсмотрим пару Best Practices от команд, которые пишут на Kotlin в продакшене.
1. Строки: больше, чем просто текст
В Kotlin строки — это объекты класса String. Они неизменяемы (как и в Java), и любая операция «изменения» на самом деле создаёт новую строку.
Получение подстрок
Частая задача — вытащить часть строки. В Kotlin для этого есть удобные методы:
fun main() { val url = "https://otus.ru/kotlin-basic" println(url.substring(8)) // otus.ru/kotlin-basic println(url.substring(8, 15)) // otus.ru println(url.substringAfter("://")) // otus.ru/kotlin-basic println(url.substringBefore("/", "https://")) // https: }
substringBefore и substringAfter — настоящие палочки-выручалочки, когда парсишь логи или URL. В Java пришлось бы писать indexOf и проверять на -1.
Форматирование строк
Мы уже знакомы с шаблонами $name, но есть ещё метод format, который удобен для сложных шаблонов:
val template = "Студент %s набрал %d баллов из %.2f возможных" println(template.format("Иван", 42, 100.0))
Но мой личный фаворит — многострочные строки с trimMargin(). Представьте, что нужно сгенерировать JSON или SQL-запрос. Вот как это делается красиво:
val json = """ { "name": "Сергей", "course": "Kotlin Basic", "platform": "OTUS" } """.trimIndent()
Обработка строк: CharSequence
String реализует интерфейс CharSequence, поэтому все методы CharSequence доступны. Например, lines() разбивает текст на строки (удобно для обработки CSV или логов), а split() возвращает список подстрок.
val data = "apple,banana,orange" val fruits = data.split(",") // List<String>
Строковые алгоритмы: проверка палиндрома
Палиндром — это слово, фраза или число, которые одинаково читаются в обоих направлениях. Классические примеры: «топот», «racecar», «12321». На собеседованиях эту задачу любят давать новичкам: она простая по сути, но проверяет понимание работы со строками, неизменяемость объектов и умение «нормализовать» входные данные.
Представьте, что вам приходит строка от пользователя, и в ней могут быть лишние пробелы, разный регистр букв и даже знаки препинания. Прежде чем сравнивать символы, строку нужно «очистить» — привести к единому формату. Вот как это выглядит в Kotlin:
fun isPalindrome(s: String): Boolean { val cleaned = s.replace("\\s".toRegex(), "").lowercase() return cleaned == cleaned.reversed() }
Как работает код построчно:
s.replace("\\s".toRegex(), "")— удаляем все пробельные символы (обычные пробелы, табуляцию, переносы строк). Регулярное выражение\sищет любой пробельный символ. В Kotlin его нужно экранировать обратным слэшем, поэтому пишем"\\s".toRegex(). Если оставить пробелы, строка"топот"и"топ от"будут считаться разными, хотя по сути это один палиндром..lowercase()— приводим всё к нижнему регистру. Без этого"Racecar"не совпадёт с"racecaR", ведь символы 'R' и 'r' имеют разные коды. Для проверки палиндрома регистр не важен.val cleaned— теперь у нас «чистая» строка, готовая к сравнению.cleaned.reversed()— метод Kotlin, который возвращает новую строку с символами в обратном порядке. Например,"hello".reversed()даст"olleh".return cleaned == cleaned.reversed()— сравниваем оригинальную очищенную строку с её перевёрнутой версией. Если равны — палиндром.
Почему это работает? Строки в Kotlin неизменяемы, поэтому replace и lowercase создают новые объекты, не меняя исходную s. Это безопасно и предсказуемо.
Что можно улучшить для реального проекта? Если строка может содержать знаки препинания (например, "А роза упала на лапу Азора."), стоит добавить замену всех небуквенных символов. Например, так:
val cleaned = s.replace("[^\\p{L}\\p{Nd}]".toRegex(), "").lowercase()
Но для собеседования и базового понимания достаточно показанного варианта. Главное — объяснить, что вы осознаёте необходимость нормализации данных перед сравнением.
2. Коллекции строк: List и MutableList
Коллекции — сердце любого приложения. В Kotlin чётко разделены read-only интерфейсы и mutable реализации.
List— только для чтения. Вы не можете добавить или удалить элементы.MutableList— можно изменять.
val readOnlyList = listOf("Kotlin", "Java", "Python") // readOnlyList.add("C++") // Ошибка компиляции! val mutableList = mutableListOf("Kotlin", "Java") mutableList.add("C++")
Важный нюанс: val перед MutableList означает, что ссылку изменить нельзя, но содержимое — можно. Это часто путают новички.
val myList = mutableListOf(1, 2, 3) myList.add(4) // OK // myList = mutableListOf(5, 6) // Ошибка, val не позволяет переприсвоить ссылку
Работа с MutableList
В отличие от неизменяемого List, MutableList позволяет добавлять, удалять и изменять элементы прямо в существующем списке. Это основной инструмент, когда вы заранее не знаете точный набор данных или планируете его менять в процессе работы программы.
Рассмотрим базовые операции на примере:
val numbers = mutableListOf(1, 2, 3) println("Исходный список: $numbers") // [1, 2, 3] numbers.add(4) // Добавление элемента в конец println("После add(4): $numbers") // [1, 2, 3, 4] numbers.add(0, 0) // Добавление по индексу (сдвиг вправо) println("После add(0, 0): $numbers") // [0, 1, 2, 3, 4] numbers.removeAt(1) // Удаление элемента с индексом 1 (значение 1) println("После removeAt(1): $numbers") // [0, 2, 3, 4] numbers.remove(2) // Удаление первого вхождения значения 2 println("После remove(2): $numbers") // [0, 3, 4] numbers[0] = 99 // Изменение элемента по индексу (присваивание) println("После numbers[0] = 99: $numbers") // [99, 3, 4]
Что важно запомнить:
add(element)— простая операция, аналогlist.add()в Java. Элемент помещается в конец списка, размер увеличивается на 1.add(index, element)— вставляет элемент на указанную позицию, сдвигая все последующие элементы вправо. Индекс должен быть в диапазоне от0доsize(включительно, чтобы добавить в конец).removeAt(index)— удаляет элемент по индексу и возвращает его значение. Индекс должен быть в пределах0..lastIndex, иначе будетIndexOutOfBoundsException.remove(element)— удаляет первое вхождение заданного значения и возвращаетtrue, если элемент был найден и удалён. Если элемента нет — возвращаетfalse, список не меняется.numbers[index] = newValue— присваивание по индексу заменяет существующий элемент новым значением. Аналогset(index, element).
Best Practice:
В промышленном коде мы стараемся минимизировать использование изменяемых коллекций в публичных API — предпочтение отдаётся неизменяемым List и функциям-преобразованиям (map, filter). Но внутри приватных методов MutableList незаменим для построения данных.
Этот код легко скопировать в IntelliJ IDEA и выполнить по шагам, чтобы увидеть эволюцию списка своими глазами.
3. Циклы и списки: for и withIndex
В прошлой статье мы рассматривали циклы по диапазонам. Теперь применим их к коллекциям.
val languages = listOf("Kotlin", "Java", "Scala") // Простой перебор for (lang in languages) { println(lang) } // Если нужен индекс for (i in languages.indices) { println("$i -> ${languages[i]}") } // Идиоматичный Kotlin: withIndex() for ((index, value) in languages.withIndex()) { println("$index -> $value") }
Последний вариант наиболее выразителен и читаем. В промышленном коде мы используем именно его.
4. Массивы: когда нужен фиксированный размер
Массивы в Kotlin представлены классом Array<T>. Они имеют фиксированный размер, но элементы можно менять.
val intArray = arrayOf(1, 2, 3) val stringArray = arrayOf("a", "b", "c") // Доступ по индексу intArray[0] = 10
Для примитивов есть специализированные классы: IntArray, DoubleArray и т.д. Они эффективнее по памяти.
val squares = IntArray(5) { i -> i * i } // [0, 1, 4, 9, 16]
Строковые массивы и циклы
Часто возникает задача преобразовать строку в массив слов или символов:
val sentence = "Kotlin is awesome" val words: Array<String> = sentence.split(" ").toTypedArray() val chars: CharArray = sentence.toCharArray() for (word in words) { println(word) }
5. Enum: перечисления с суперсилой
Enum в Kotlin — это не просто константы, как в Java. Они могут иметь свойства, методы и даже реализовывать интерфейсы.
Классический пример — HTTP-статусы:
enum class HttpStatus(val code: Int, val description: String) { OK(200, "OK"), NOT_FOUND(404, "Not Found"), INTERNAL_SERVER_ERROR(500, "Internal Server Error"); fun isSuccess(): Boolean = code in 200..299 } fun main() { val status = HttpStatus.NOT_FOUND println("${status.code} - ${status.description}") println("Is success? ${status.isSuccess()}") }
Такой подход позволяет инкапсулировать связанные данные прямо в перечислении, делая код самодокументированным.
Визуализация потока данных
Чтобы лучше понять, как строки и коллекции взаимодействуют в типичной задаче обработки данных, взглянем на схему (рис. 1):

Что следует из этой схемы? Перед нами универсальный «конвейер», через который проходят данные в 90% бизнес-приложений. Осознав его, вы перестаёте писать разрозненные куски кода и начинаете выстраивать обработку как цепочку преобразований.
Практический смысл здесь вот в чём:
Разделение ответственности. Каждый этап делает ровно одну вещь:
splitразбивает,mapпреобразует,filterотсеивает. Такой код легко тестировать и отлаживать. Если в отчёте появилось лишнее поле, вы сразу знаете, что проверять этап маппинга, а не рыться во всей программе.Неизменяемость данных. В Kotlin коллекции по умолчанию неизменяемы, и каждая операция создаёт новую коллекцию. Это исключает побочные эффекты — классическую проблему, когда где-то в глубине кода список неожиданно меняется, и вы тратите часы на дебаг.
Декларативный стиль. Вместо инструкций «сделай шаг 1, потом шаг 2» вы описываете, чтодолжно получиться. Это повышает читаемость и снижает порог входа для новых членов команды.
Гибкость и переиспользование. Поменять источник данных с CSV на JSON? Достаточно заменить первый этап (парсинг), остальная цепочка останется без изменений. Нужно добавить кэширование? Вставляете ещё один шаг в конвейер.
В реальных проектах я не раз видел, как такой подход сокращал объём кода вдвое, а главное — делал его понятным даже спустя полгода. Помню случай: джуниор написал обработку логов через цепочку split → map → filter → joinToString, и когда через три месяца потребовалось добавить новый фильтр, он просто вставил ещё одно условие в filter, не переписывая весь метод. Это и есть сила функционального конвейера в Kotlin.
Что дальше?
Сегодня мы разобрали инструменты, которые будут с вами каждый день: строки, коллекции, массивы и enum. Это база, без которой невозможно написать ни один микросервис, ни одно мобильное приложение.
В следующих статьях мы перейдём к лямбдам, функциям высшего порядка и, конечно, корутинам — теме, которая открывает двери в высоконагруженные асинхронные системы.
Если вы хотите системно освоить Kotlin, понять не только синтаксис, но и архитектурные паттерны, научиться писать чистый, поддерживаемый код — в OTUS есть целое направление по Kotlin: бэкенд, Android, QA — каждый найдёт свой трек.
Серия статей «Kotlin для новичков»:
Если базовый синтаксис уже не пугает, обычно возникает следующий запрос: не просто писать рабочий код, а лучше понимать, как устроены реальные задачи, какие подходы считаются нормой в продакшене и где заканчиваются учебные примеры.
В таких случаях полезно смотреть не отдельные материалы, а весь каталог курсов по программированию — чтобы выбрать трек под свой текущий уровень и рабочие задачи.