JavaRush /Курсы /Kotlin SELF /Индексы и подстроки в Kotlin: length, lastIndex, indexOf,...

Индексы и подстроки в Kotlin: length, lastIndex, indexOf, substring

Kotlin SELF
9 уровень , 3 лекция
Открыта

1. Строка как последовательность символов

Когда вы начинаете «разбирать» строку, мозг автоматически хочет относиться к ней как к тексту из книги. Но компьютеру книга не нужна — ему нужна последовательность символов, у которой каждый символ лежит на своём месте и имеет номер. Этот номер называется индексом, и в Kotlin (как и почти везде) индексация начинается с нуля.

Индексы с нуля: почему первый символ — это 0

Идея «первый элемент имеет индекс 0» сначала кажется издевательством, но быстро становится удобной: индекс = «сколько символов стоит до текущего». До первого символа стоит ноль символов — значит, индекс 0. До второго стоит один — значит, индекс 1. Это ещё пригодится, когда будем вырезать substring().

Посмотрим на строку "Kotlin":

Символ
K
o
t
l
i
n
Индекс
0
1
2
3
4
5

Количество символов — 6, а последний индекс — 5.

length и lastIndex: не путать «размер» и «последний индекс»

Свойство length говорит, сколько символов в строке. А lastIndex — это индекс последнего символа.

Важно почувствовать разницу: length — это «количество», lastIndex — это «последняя позиция». В коллекциях и массивах используется ровно та же логика: последний индекс равен size - 1.

Мини‑пример:

fun main() {
    val s = "Kotlin"

    println(s.length)      // 6
    println(s.lastIndex)   // 5
}

Если строка пустая (""), то length будет 0, а lastIndex станет -1. Это звучит странно, но логично: «последнего символа нет», поэтому «последний индекс» уходит в отрицательную область. Именно из-за этого мы так много говорим про проверки границ.

2. Доступ к символу по индексу: s[i] и аккуратная проверка границ

Теперь, когда индексы есть, руки тянутся сделать так: «дай мне символ номер 0». И Kotlin действительно позволяет обращаться к символу по индексу через квадратные скобки. Но есть подвох: если индекс неверный, программа упадёт с исключением (а вам нужно писать код, который не падает от одного пробела не там).

s[i] возвращает Char: текст — это не всегда строка

Оператор s[i] возвращает один символ типа Char. Символ — это не строка. Строка — это много символов.

fun main() {
    val s = "Kotlin"

    val first: Char = s[0]
    val last: Char = s[s.lastIndex]

    println(first) // K
    println(last)  // n
}

Если вы попробуете сделать s[6] для "Kotlin", программа упадёт: индексы там только 0..5.

Проверка границ: «перед тем как взять — убедись, что можно»

Здесь полезно держать в голове короткое правило: допустимые индексы — это все числа, которые не меньше 0 и меньше length.

Мини‑хелпер, который безопасно возвращает символ или '?', если индекс плохой:

fun charOrQuestion(text: String, index: Int): Char {
    if (index < 0 || index >= text.length) return '?'
    return text[index]
}

fun main() {
    println(charOrQuestion("Kotlin", 2))   // t
    println(charOrQuestion("Kotlin", 99))  // ?
}

Пустая строка: самый маленький, но очень коварный кейс

Пустая строка — это строка длины 0. У неё нет ни первого, ни последнего символа. Это значит, что даже s[0] опасен.

fun main() {
    val s = ""

    println(s.length)    // 0
    println(s.lastIndex) // -1
    println(s[0])        // так делать нельзя — будет ошибка
}

Хорошая привычка, которая спасает нервы: перед доступом к s[0] спрашивать себя: «а строка точно не пустая?»

3. Поиск в строке: indexOf() и проверки contains(), startsWith(), endsWith()

Обычно мы разбираем строки не от скуки, а потому что в строке есть разделитель: пробел, @, =, двоеточие, точка и так далее. Сценарий часто такой: «найди, где стоит разделитель, и разрежь строку на две части».

indexOf(...): договорённость про -1

Метод indexOf(...) ищет подстроку (или символ) и возвращает индекс первого вхождения. А если не нашёл — возвращает -1. Это очень важная договорённость: -1 означает «не найдено».

fun main() {
    val email = "user@example.com"

    val at = email.indexOf('@')
    println(at) // 4

    val slash = email.indexOf('/')
    println(slash) // -1
}

Если вы забудете проверить -1 и начнёте резать строку по этому индексу, получится либо ошибка границ, либо очень странный результат.

contains(), startsWith(), endsWith(): когда не нужны индексы

Иногда вам не нужен индекс — вам нужно просто «да/нет». Тогда удобнее использовать методы‑проверки.

fun main() {
    val filename = "report.txt"

    println(filename.contains("."))      // true
    println(filename.startsWith("rep"))  // true
    println(filename.endsWith(".txt"))   // true
}

Эти методы отвечают на простые вопросы:

  • contains() — содержится ли в строке переданная подстрока?
  • startsWith() — строка начинается с переданной подстроки?
  • endsWith() — строка заканчивается на переданную подстроку?

Эти проверки часто делают код безопаснее: вы меньше вручную лезете в индексы, меньше шансов промахнуться «на 1».

4. substring(): режем строку на части, не наступая на грабли

Если индексы — это «координаты» в строке, то substring() — это «ножницы». Почти любой разбор строки сводится к одному шаблону: найти индекс разделителя и вырезать левую/правую часть.

Главное правило substring(start, end): end не входит

Метод substring(start, end) берёт символы с позиции start включительно, а вот endне включается. Это классический стиль «полуинтервала», и он невероятно удобен, но в начале путает.

Схема:

"Kotlin"
 012345

substring(0, 3) -> берём индексы 0,1,2 -> "Kot"

Пример:

fun main() {
    val s = "Kotlin"

    println(s.substring(0, 3)) // Kot
    println(s.substring(3, 6)) // lin
}

Заметьте: end может быть равен length, и это нормально. «До конца строки» — это как раз end = length.

substring(start) — от позиции до конца

Есть удобная форма substring(start) — она берёт от start до конца строки.

fun main() {
    val s = "Kotlin"

    println(s.substring(2)) // tlin
}

Это очень удобно для правой части после разделителя: «после @ до конца», «после = до конца».

Границы substring(): где программа может упасть

substring() требует корректных границ. Упрощённо: start и end должны быть в диапазоне 0..length, и start <= end. Если нарушить — будет ошибка.

Поэтому опытный программист почти никогда не пишет substring(...) «в лоб»: он сначала проверяет, что индексы нормальные.

5. Паттерн «нашёл → проверил → вырезал»

Сейчас мы соберём всё в один устойчивый паттерн и добавим его в наше учебное консольное приложение. Представим, что мы делаем простую программу «Профиль пользователя»: пользователь вводит полное имя одной строкой и email, а мы выводим аккуратную карточку. Сегодня мы разберёмся именно с тем, как безопасно получить части этих строк через индексы.

Наша реальность сурова: люди вводят имя как попало. Да, они ставят пробелы странно. Да, они иногда забывают @. Это не «плохие пользователи», это «реальный мир».

Разбираем полное имя "Имя Фамилия" через indexOf(' ')

Самая простая версия полного имени — это два слова, разделённые пробелом. Мы пока специально не используем split() (это будет в следующей лекции), поэтому сделаем «ручной разрез»: найдём индекс пробела и вырежем левую и правую части.

fun main() {
    val fullName = "Иван Петров"
    val space = fullName.indexOf(' ')

    val firstName = if (space == -1) fullName else fullName.substring(0, space)
    val lastName = if (space == -1) "" else fullName.substring(space + 1)

    println(firstName) // Иван
    println(lastName)  // Петров
}

Обратите внимание на space + 1: мы не хотим, чтобы пробел попал в фамилию. Если пробел — последний символ строки, substring(space + 1) превратится в substring(length), а это просто пустая строка (и это нормально).

Достаём домен из email: indexOf('@') и проверки

Email — классический пример строки с разделителем. Мы хотим взять домен: всё, что стоит после @. Но нужно защититься от случаев "@example.com" и "user@".

fun extractDomain(email: String): String {
    val at = email.indexOf('@')
    if (at == -1) return "INVALID"
    if (at == 0) return "INVALID"
    if (at == email.lastIndex) return "INVALID"

    return email.substring(at + 1)
}

fun main() {
    println(extractDomain("user@example.com")) // example.com
    println(extractDomain("user@"))            // INVALID
}

Здесь мы используем email.lastIndex, чтобы проверить: «@ стоит последним символом или нет». И это как раз тот случай, когда lastIndex читается лучше, чем length - 1.

Разбираем строку формата "key=value"

Очень популярный формат ввода — ключ=значение. Сегодня мы сделаем простую версию: найдём = и разрежем строку.

fun main() {
    val line = "city=Tokio"
    val eq = line.indexOf('=')

    val key = if (eq == -1) "" else line.substring(0, eq)
    val value = if (eq == -1) "" else line.substring(eq + 1)

    println("key='$key', value='$value'") // key='city', value='Tokio'
}

Паттерн тот же: нашли индекс → проверили -1 → вырезали левую и правую части.

Собираем «карточку профиля» и не путаем разбор с выводом

Теперь соберём маленький фрагмент нашего приложения: читаем fullName и email, вычисляем firstName, lastName, domain и печатаем карточку.

fun extractDomain(email: String): String {
    val at = email.indexOf('@')
    if (at <= 0 || at == email.lastIndex) return "INVALID"
    return email.substring(at + 1)
}

fun main() {
    print("Введите имя и фамилию: ")
    val fullName = readln()

    print("Введите email: ")
    val email = readln()

    val space = fullName.indexOf(' ')
    val firstName = if (space == -1) fullName else fullName.substring(0, space)
    val lastName = if (space == -1) "" else fullName.substring(space + 1)

    val domain = extractDomain(email)

    val card = """
        |Профиль
        |-------
        |Имя: $firstName
        |Фамилия: $lastName
        |Домен email: $domain
    """.trimMargin()

    println(card)
}

Здесь логика разбора (индексы, indexOf(), substring()) находится рядом, но не перемешивается в одну гигантскую строку. Такой стиль проще отлаживать: если карточка странная, вы можете распечатать промежуточные значения space, firstName, lastName.

Шпаргалка по индексам и проблемным местам

Когда вы работаете со строками, полезно иметь в голове маленькую табличку‑шпаргалку: что возвращается и что обязательно проверять.

Операция Что возвращает На что смотреть, чтобы не упасть
s.length
количество символов если 0, строка пустая
s.lastIndex
индекс последнего символа будет -1, если строка пустая
s[i]
Char
по индексу
i
должен быть
0 <= i < length
s.indexOf(x)
индекс или -1 -1 обязательно обработать
s.substring(a, b)
кусок строки
a
и
b
в
0..length
, и
a <= b
s.substring(a)
от
a
до конца
a
должен быть в
0..length

И ещё один важный «мостик» к пониманию: индексы, начинающиеся с нуля, — это общий принцип Kotlin для индексируемых структур. Он одинаково проявляется и в массивах, и в списках, и в циклах по indices.

6. Типичные ошибки при работе с индексами и подстроками

Ошибка №1: путать length и «последний индекс».
Очень частый промах — думать, что раз длина строки 6, то последний индекс 6. На самом деле при индексации с нуля последний индекс всегда на 1 меньше. Из-за этого появляются ошибки «на 1»: вы либо берёте не тот символ, либо падаете на выходе за границы.

Ошибка №2: делать s[0] без проверки на пустую строку.
Пустая строка встречается чаще, чем кажется: пользователь нажал Enter, пришёл пустой ввод, где-то склеилось в "". Если после этого вы делаете text[0], программа падает. Правильная привычка — сначала проверять text.isEmpty() или хотя бы text.length == 0, и только потом лезть в индексы.

Ошибка №3: игнорировать indexOf(...) == -1.
indexOf не «ругается», он честно возвращает -1. Но если вы тут же используете этот -1 как границу в substring(), начинается хаос: в лучшем случае будет исключение, в худшем — вы отрежете что-то не то и получите «тихо неправильный» результат. Поэтому связка «нашёл → проверил → вырезал» должна стать рефлексом.

Ошибка №4: забыть, что substring(start, end) не включает end.
Это источник ошибок, которые особенно раздражают: программа вроде работает, но выдаёт «почти правильный» ответ — на один символ короче или длиннее. Лучшее лекарство — всегда проговаривать: «start включительно, end не включается», и делать маленькую схему на бумаге, когда сомневаетесь.

Ошибка №5: вырезать правую часть, не убрав разделитель (substring(at) вместо substring(at + 1)).
Если вы делите "user@example.com" по @, то правую часть вы хотите без @. Это значит substring(at + 1). Если написать substring(at), домен начнётся с @, и дальше логика «валидности» и сравнения строк может начать вести себя странно. Это особенно неприятно в программах, где потом домен сравнивают с "gmail.com".

Ошибка №6: смешивать разбор строки и форматирование результата в одну огромную строку.
Технически можно написать один гигантский println("Имя: ${...} ..."), но это почти гарантированно приведёт к ошибке и плохой читаемости. Надёжнее сначала получить val firstName, val lastName, val domain, а уже потом собрать финальный вывод. Такой код проще читать, проще тестировать и проще чинить, когда пользователь снова введёт что-то «творческое».

1
Задача
Kotlin SELF, 9 уровень, 3 лекция
Недоступна
Паспорт строки
Паспорт строки
1
Задача
Kotlin SELF, 9 уровень, 3 лекция
Недоступна
Символ по индексу
Символ по индексу
1
Задача
Kotlin SELF, 9 уровень, 3 лекция
Недоступна
Поиск разделителя
Поиск разделителя
1
Задача
Kotlin SELF, 9 уровень, 3 лекция
Недоступна
Имя и фамилия
Имя и фамилия
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ