JavaRush /Курси /Kotlin SELF /Перетворення типів і робота з Char

Перетворення типів і робота з Char

Kotlin SELF
Рівень 6 , Лекція 5
Відкрита

1. Вступ

Коли ви починаєте писати на Kotlin, він іноді здається суворим учителем: «Ні, так не можна. І так не можна. І це не можна. І взагалі — чому ви так дивитеся?» Насправді це не занудство, а захист від тихих помилок, які в інших мовах можуть непомітно змінювати сенс програми. У Kotlin майже будь-які перетворення числових типів виконуються явно — тобто ви самі пишете, чого хочете.

Уявіть: ви рахуєте знижку у відсотках, зберігаєте її як Double, а потім десь «випадково» перетворюєте на Int — і дробова частина зникає. Якщо мова робить це автоматично, ви можете тижнями жити з помилкою «знижка іноді дивна», доки не спіймаєте потрібний момент. Kotlin змушує вас написати .toInt() і тим самим ніби запитує: «Ви точно хочете викинути дріб? Справді точно-точно?»

Це правило стосується не лише дробів. Kotlin не любить «неявного розширення» на кшталт Int Double, навіть якщо за змістом воно безпечне. Тому замість «магії» у нас є прості й читабельні методи: toDouble(), toFloat(), toInt(), а у світі символів — ще й звʼязок Char число.

2. Базові перетворення чисел

Коли ви бачите виклик на кшталт x.toInt(), корисно думати не «Kotlin щось там зробив», а «я попросив Kotlin виконати конкретну операцію». Це важлива звичка: якщо ви не формулюєте дію, потім буде складно пояснити собі (і наставнику), чому відповідь «попливла».

Нижче — проста табличка, яка допомагає тримати в голові суть базових перетворень:

Перетворення Приклад Що відбувається Де може бути втрата даних
Int → Double
5.toDouble()
число стає дробовим, значення зберігається зазвичай ні
Int → Float
5.toFloat()
число стає дробовим, але Float менш точний інколи так (на великих числах)
Double → Int
3.99.toInt()
дробова частина відкидається (не заокруглюється!) так, завжди, якщо є дріб
Double → Float
1e50.toFloat()
може стати Infinity або сильно спотворитися так
Float → Double
1.5f.toDouble()
точність не «відновлюється», але тип стає Double дані вже могли бути втрачені раніше

Зараз ми не заглиблюємося в біти й будову памʼяті (це окрема тема), але практичний висновок простий: перетворення «вниз» за розміром типу (наприклад, Double Int, Double Float) — майже завжди потенційно небезпечне.

Подивіться, як це виглядає в коді.

fun main() {
    val x = 10
    val d = x.toDouble()

    println("x = $x")     // x = 10
    println("d = $d")     // d = 10.0
}

Тут усе чудово: Int Double зберігає значення — просто додає «можливість мати дробову частину».

А тепер — приклад, де дані втрачаються:

fun main() {
    val price = 199.99
    val asInt = price.toInt()

    println("price = $price")   // price = 199.99
    println("asInt = $asInt")   // asInt = 199
}

Останнє нагадування: toInt() не заокруглює, а саме відрізає дробову частину (по суті, рухається до нуля). Якщо ви хочете заокруглити «по-людськи», зробіть це окремим кроком (ви вже знаєте round/floor/ceil з попередньої лекції), і лише потім приводьте тип.

3. Змішування типів і непомітна втрата сенсу

Коли ви пишете невеликі навчальні програми, може здаватися, що перетворення типів — це рідкість. Але щойно зʼявляються «справжні» обчислення (середнє, відсоток, імовірність, гроші), раптом виявляється, що половина помилок — це не if і не цикли, а неправильний тип у неправильному місці.

Дуже типовий сценарій: хочемо отримати середнє значення. У нас є сума (Int) і кількість (Int). Якщо поділити як Int / Int, дробова частина зникне. Тому потрібно явно перейти до Double хоча б в одній частині виразу.

fun main() {
    val sum = 7
    val count = 2

    val avg1 = sum / count
    val avg2 = sum.toDouble() / count

    println("avg1 = $avg1")  // avg1 = 3
    println("avg2 = $avg2")  // avg2 = 3.5
}

Формуйте програмістське мислення: ми не «виправляємо Kotlin», а задаємо правило обчислення. Хочете дріб — робіть дріб.

Ще один частий випадок — змішування Float і Double. Kotlin не любить, коли ви випадково перемішали два різні дробові типи. Зазвичай простіше обрати Double як основний, а Float використовувати лише тоді, коли є чітка причина.

4. Char: один символ і чому це корисно

Тепер переходимо до нового типу — Char. У Kotlin Char — це один символ, який записують в одинарних лапках: 'A', '7', '?'. Літерали Char записують в одинарних лапках, а спеціальні символи задають через escape-послідовності.

Чому це важливо? Тому що новачки часто плутають:

  • '7' — символ сім (тип Char)
  • "7" — рядок довжини 1 (тип String)
  • 7 — число (тип Int)

І це три різні сутності. Іноді їх можна взаємно перетворювати, але автоматично Kotlin цього не робить, щоб ви ненароком не наробили дурниць (таке трапляється з усіма — особливо на старті).

Ще Char часто трапляється в рядках: рядок — це послідовність символів, і ви можете брати окремі символи за індексом.

Escape-символи: '\n', '\t' та інші

Після знайомства з рядками корисно зрозуміти, що деякі символи складно записати «як є». Тому існують escape-послідовності: '\n' — новий рядок, '\t' — табуляція тощо. Вони теж належать до Char.

fun main() {
    val a = 'A'
    val tab = '\t'
    val nl = '\n'

    println("[$a]")            // [A]
    println("x${tab}y")        // x    y
    println("line1${nl}line2") // line1
                               // line2
}

Тут tab і nl — це саме символи, а не «магічні команди» для друку.

5. Char і числа: коди, toChar() та цифри

Зараз буде шматочок, який зробить вас трохи більш «компʼютерними шаманами». Ідея проста: кожен символ має числовий код. На JVM Char — це 16-бітний Unicode-символ. Це означає, що символ можна розглядати як число — і навпаки: число можна перетворити на символ (якщо воно потрапляє в допустимий діапазон).

У Kotlin у символу є властивість code, яка повертає числовий код символу (Int). А зворотна операція — toChar() у числа.

fun main() {
    val ch: Char = 'A'
    val code: Int = ch.code
    val back: Char = code.toChar()

    println("ch = $ch")        // ch = A
    println("code = $code")    // code = 65
    println("back = $back")    // back = A
}

Це виглядає як фокус, але насправді тут просто два взаємні перетворення.

Найпопулярніший практичний приклад — цифри. Введення з клавіатури часто приходить як текст. Навіть якщо ви не парсите весь Int, іноді потрібно взяти одну цифру як символ, перевірити, що це цифра, і перетворити її на число.

Крім того, з кодами можна робити «зсуви»: 'A' + 1 'B', '0' + 7 '7'. У Kotlin така ідея трапляється, наприклад, у задачах, де символи порівнюють і перетворюють на числа.

«Символ цифри» → число без магії

Перетворення '7' на число 7 — це класика. Новачки часто намагаються зробити щось на кшталт:

  • "7".toInt() — це працює для рядка, але не для Char
  • ('7').toInt() — так не можна, бо Char не перетворюється на «значення цифри» автоматично
  • 7.toChar() — це взагалі в інший бік і дасть не '7', а символ із кодом 7 (а це керувальний символ)

Правильний підхід такий: якщо у нас є символ ch і ми впевнені, що він у діапазоні '0'..'9', то значення цифри дорівнює різниці кодів: digit = ch.code - '0'.code

Спочатку перевіримо, що це справді цифра.

fun main() {
    val ch = '7'

    val isDigit = ch >= '0' && ch <= '9'
    val digit = if (isDigit) ch.code - '0'.code else -1

    println("isDigit = $isDigit")  // isDigit = true
    println("digit = $digit")      // digit = 7
}

Тут -1 — просто «сигнал помилки» в межах прикладу: якщо це не цифра, ми повертаємо значення, яке точно не може бути цифрою. Пізніше, коли в нас зʼявляться серйозніші підходи до помилок, ми робитимемо інакше, але поки цього достатньо.

Важливо помітити: такий стиль (порівняння символів, робота з діапазонами й перетворення через коди) — не вигадка заради краси. Він справді трапляється в реальному Kotlin-коді, особливо в алгоритмічних задачах.

6. Мінісценарій: генеруємо купон із літер і цифр

Уявімо, що ми робимо консольний мінізастосунок LuckyCoupon. Він показує користувачу знижку у відсотках і код купона формату A7Q2 (літера–цифра–літера–цифра). Жодних циклів — просто чотири кроки, щоб не відволікатися від суті.

Ключова ідея: щоб отримати випадкову літеру, беремо випадкове число 0..25, додаємо його до коду 'A' і перетворюємо назад у Char.

import kotlin.random.Random

fun main() {
    val letter1 = ('A'.code + Random.nextInt(0, 26)).toChar() // 'A'..'Z'
    val digit1  = ('0'.code + Random.nextInt(0, 10)).toChar() // '0'..'9'

    val letter2 = ('A'.code + Random.nextInt(0, 26)).toChar()
    val digit2  = ('0'.code + Random.nextInt(0, 10)).toChar()

    val code = "$letter1$digit1$letter2$digit2"
    println("Ваш купон: $code") // приклад: A7Q2
}

Зверніть увагу на дві важливі речі.

Перша: тут немає «магії перетворень». Ми явно працюємо з кодами через .code і toChar(). Це робить код трохи довшим, зате й набагато зрозумілішим: із числа зробили код символу, з коду зробили символ.

Друга: ми акуратно використовуємо межі nextInt(0, 26) і nextInt(0, 10). Верхня межа не включається, а отже 26 літер латинського алфавіту ми покрили рівно й без сюрпризів.

Знижка й місце, де часто губиться дріб

Нехай знижка обчислюється як Double (наприклад, вам випала «плаваюча» знижка). Для виводу ми хочемо відсоток як ціле число. Тут важливо визначитися: ми відріжемо дріб чи заокруглимо? Зараз покажемо саме втрату дробу, щоб ви бачили різницю.

fun main() {
    val discount = kotlin.random.Random.nextDouble() * 30.0  // 0.0..30.0
    val discountInt = discount.toInt()                       // дріб відрізали

    println("discount = $discount")           // discount = 12.345678...
    println("discountInt = $discountInt%")    // discountInt = 12%
}

Якщо ви хочете заокруглювати «за правилами», тепер ви знаєте: це потрібно зробити окремим кроком.

7. Схема-підказка щодо перетворень

Іноді корисно тримати в голові не окремі методи, а невеличкий «маршрут» розвʼязання. Ось проста блок-схема, яка допомагає не плутати «відрізання дробу» і «заокруглення», а також памʼятати, що символи живуть у світі кодів.

flowchart TD
    A["Є значення"] --> B{Це число чи символ?}
    B -->|Число| C{Потрібен інший числовий тип?}
    C -->|Так| D["Явно: toDouble/toFloat/toInt"]
    C -->|Ні| E["Залишаємо як є"]
    D --> F{Переходимо з Double в Int?}
    F -->|Так| G["Розуміємо: дріб буде втрачено"]
    F -->|Ні| H["Зазвичай безпечніше"]
    B -->|Символ Char| I["Беремо ch.code (Int)"]
    I --> J{Хочемо цифру?}
    J -->|Так| K["Перевіряємо '0'..'9' і віднімаємо '0'.code"]
    J -->|Ні| L["Перетворюємо числа ↔ символи через toChar()"]

8. Типові помилки

Помилка №1: плутати '7', "7" і 7.
Це три різні типи: Char, String і Int. Якщо ви зберігаєте цифру як Char, це не число 7 і не рядок "7". Намагатися застосовувати до Char методи для рядків або очікувати, що Char сам «стане числом», — часта причина плутанини й дивних перетворень.

Помилка №2: вважати, що toInt() заокруглює.
toInt() не заокруглює, а відкидає дробову частину. Це особливо неприємно, коли ви очікуєте «знижка 12.9% стане 13%», а отримуєте 12%. Kotlin тут не винен: він зробив рівно те, що ви попросили. Якщо потрібне математичне заокруглення — спочатку заокруглюйте, потім приводьте до Int.

Помилка №3: робити Double Int «десь потім», не думаючи про сенс.
Іноді пишуть: «Поки нехай буде Double, а в кінці перетворю на Int». Це небезпечно: ви можете втратити дані в момент, коли вже запізно зрозуміти, звідки взялася помилка. Краще мати чітку домовленість: де зберігаємо точне обчислення (Double), а де саме нам потрібен цілий результат (Int), і робити перетворення в зрозумілому місці.

Помилка №4: перетворювати цифру з Char у Int без перевірки діапазону.
Вираз ch.code - '0'.code коректний лише тоді, коли ch справді в діапазоні '0'..'9'. Якщо там опиниться 'A', ви отримаєте якесь число (наприклад, 17), і програма не впаде — вона просто почне робити нісенітницю. Тому спочатку перевірка, потім перетворення.

Помилка №5: забувати, що Char — це Unicode-символ, а не «літера з ASCII-таблиці».
У навчальних прикладах ми спокійно використовуємо 'A'..'Z' і '0'..'9', і це працює передбачувано. Але важливо памʼятати, що Char у Kotlin — це Unicode, і «літери» бувають не лише латинські. Саме тому коректніше мислити в термінах «код символу» й конкретних діапазонів, ніж у термінах «ну це ж літера, отже плюс один буде наступна».

1
Опитування
Дійсні числа, рівень 6, лекція 5
Недоступний
Дійсні числа
Дійсні числа
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ