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, а вже потім зібрати фінальне виведення. Такий код простіше читати, простіше тестувати й простіше лагодити, коли користувач знову введе щось «креативне».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ