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
Задача
Kotlin SELF, 6 уровень, 5 лекция
Недоступна
Код пропуска
Код пропуска
1
Задача
Kotlin SELF, 6 уровень, 5 лекция
Недоступна
Среднее без дроби
Среднее без дроби
1
Задача
Kotlin SELF, 6 уровень, 5 лекция
Недоступна
Одна кнопка
Одна кнопка
1
Опрос
Вещественные числа, 6 уровень, 5 лекция
Недоступен
Вещественные числа
Вещественные числа
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Серега PARADOX Уровень 21
21 марта 2026
Так же и тут ('A'.code + Random.nextInt(0, 26)).toChar() 'A'.code - старт отсчета по символам = 65 Чтобы попасть в диапазон символов алфавита по ASCII 65 по 90 Вдруг кому будет не понятно Усем удачи
Серега PARADOX Уровень 21
21 марта 2026
К этой формуле добавить бы пояснение digit = ch.code - '0'.code что это 55 - 48, так как все символы идут последовательно, можно вычислять по разнице в нумерации . Кто первый раз с этим сталкивается, может не понять и пропустить