1. Introduction
When you start writing Kotlin, it can sometimes feel like a strict teacher: “No, you can’t do that. And you can’t do that either. And this isn’t allowed. And anyway, why are you looking at me like that?” In reality, it’s not nitpicking—it’s protection against silent errors that, in other languages, can quietly change the meaning of your program. In Kotlin, almost any numeric type conversion is done explicitly—meaning you write what you want yourself.
Imagine this: you calculate a discount as a percentage, store it as a Double, and then somewhere you “accidentally” turn it into an Int, and the fractional part disappears. If the language does this automatically, you can live with a bug like “the discount is sometimes weird” for weeks until you catch the right moment. Kotlin makes you write .toInt(), and by doing so it’s basically asking: “Are you sure you want to throw away the fraction? Really, really sure?”
This rule doesn’t apply only to fractions. Kotlin also dislikes “implicit widening” like Int → Double, even if it’s conceptually safe. So instead of “magic” we have simple, readable methods: toDouble(), toFloat(), toInt(), and in the world of characters, also a connection between Char ↔ numbers.
2. Basic number conversions
When you see a call like x.toInt(), it’s useful to think not “Kotlin did something,” but “I asked Kotlin to perform a specific operation.” This is an important habit: if you don’t state the operation, later it’ll be hard to explain to yourself (and a mentor) why the result “drifted.”
Below is a simple table that helps keep the meaning of basic conversions in mind:
| Conversion | Example | What happens | Where data loss can occur |
|---|---|---|---|
|
|
the number becomes fractional; the value is preserved | usually none |
|
|
the number becomes fractional, but Float is less precise | sometimes yes (for large numbers) |
|
|
the fractional part is discarded (not rounded!) | yes, always when there’s a fraction |
|
|
may become Infinity or get heavily distorted | yes |
|
|
precision does not “come back,” but the type becomes Double | data may already have been lost earlier |
We won’t dive into bits and memory layout right now (that’s a separate topic), but the practical takeaway is simple: converting down in type size (for example, Double → Int, Double → Float) is almost always potentially dangerous.
Take a look at this in code.
fun main() {
val x = 10
val d = x.toDouble()
println("x = $x") // x = 10
println("d = $d") // d = 10.0
}
Everything is fine here: Int → Double preserves the value; it just adds the “ability to have a fractional part.”
And now—a case where data is lost:
fun main() {
val price = 199.99
val asInt = price.toInt()
println("price = $price") // price = 199.99
println("asInt = $asInt") // asInt = 199
}
Final reminder: toInt() does not round; it cuts off the fractional part (effectively moving toward zero). If you want “human-friendly rounding,” you do it separately (you already know round/floor/ceil from the previous lecture), and only then convert the type.
3. Mixing types and silent loss of meaning
When you write small practice programs, it may feel like type conversions are rare. But as soon as you have a “real” computation (average, percent, probability, money), it suddenly turns out that half the bugs are not if statements or loops, but the wrong type in the wrong place.
A very typical scenario: we want an average. We have a sum (Int) and a count (Int). If you divide as Int / Int, the fractional part disappears. So you need to explicitly switch to Double in at least one part of the expression.
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
}
Start building a programmer’s mindset: we’re not “fixing Kotlin,” we’re stating the computation rule. If we want a fraction—we make it a fraction.
Another common case is mixing Float and Double. Kotlin doesn’t like it when you accidentally mix two different floating-point types. It’s usually easier to choose Double as the main type and use Float only when there’s a clear reason.
4. Char: one character and why it’s useful
Now we move to a new type: Char. In Kotlin, Char is a single character, written in single quotes: 'A', '7', '?'. Char literals use single quotes, and special characters are specified via escape sequences.
Why is this important? Because beginners often confuse:
- '7' — the character seven (type Char)
- "7" — a string of length 1 (type String)
- 7 — a number (type Int)
And those are three different entities. Sometimes they can be converted into each other, but Kotlin doesn’t do it automatically so you don’t do something silly by accident (and you would—I know you; we’re all like that).
Also, Char often appears in strings: a string is a sequence of characters, and you can take individual characters by index.
Escape characters: '\n', '\t', and others
After learning about strings, it’s helpful to understand that some characters are hard to write “as is.” That’s why we have escape sequences: '\n' is a new line, '\t' is a tab, etc. They also belong to 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
}
Here, tab and nl are exactly characters, not magic printing commands.
5. Char and numbers: codes, toChar(), and digits
Now comes a bit that makes you slightly more of a “computer shaman.” The idea is simple: a character has a numeric code. On the JVM, Char is a 16-bit Unicode character. That means you can treat a character as a number—and vice versa, you can turn a number into a character (if it’s within the valid range).
In Kotlin, a character has the code property that gives the numeric code of the character (Int). And the reverse operation is toChar() on a number.
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
}
It looks like a trick, but it’s really just two inverse conversions.
The most popular practical example is digits. Keyboard input often comes as text. Even if you’re not parsing an entire Int, sometimes you need to take a single digit as a character, verify it’s a digit, and convert it to a number.
Also, with codes you can do “shifts”: 'A' + 1 → 'B', '0' + 7 → '7'. In Kotlin, this idea shows up, for example, in tasks where characters are compared and converted as numbers.
“Digit character” → number without magic
Converting '7' to the number 7 is a classic. Beginners often try something like:
- "7".toInt() — this works for a string, but not for a Char
- ('7').toInt() — you can’t do that, because Char doesn’t automatically turn into the “digit value”
- 7.toChar() — that’s the other direction and won’t give '7', but a character with code 7 (a control character)
The correct approach is: if we have a ch character, and we’re sure it’s in the range '0'..'9', then the digit value equals the difference of codes: digit = ch.code - '0'.code
First, let’s check that it really is a digit.
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
}
Here -1 is just an “error signal” within this example: if it’s not a digit, we return a value that definitely can’t be a digit. Later, when we have more serious approaches to errors, we’ll do it differently—but for now, this is enough.
It’s important to notice that this style (comparing characters, working with ranges, and converting via codes) is not made up for aesthetics— it really appears in real Kotlin code, especially in algorithmic tasks.
6. Mini-scenario: generate a coupon from letters and digits
Let’s imagine we’re building a console mini-app called LuckyCoupon. It prints a discount percentage and a coupon code in the format A7Q2 (letter–digit–letter–digit). No loops—just four steps so we don’t get distracted from the core idea.
Key idea: to get a random letter, we take a random number 0..25, add it to the code of 'A', and convert back to 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("Your coupon: $code") // example: A7Q2
}
Notice two important things.
First: there’s no “conversion magic” here. We explicitly work with codes via .code and toChar(). This makes the code a bit longer, but much clearer: from number to character code, from code to character.
Second: we carefully use the bounds nextInt(0, 26) and nextInt(0, 10). The upper bound is exclusive, which means we cover exactly 26 letters of the Latin alphabet—precisely and without surprises.
Discount and the place where the fraction often gets lost
Let’s say the discount is computed as a Double (for example, you got a “floating” discount). For output, we want the percent as an integer. It’s important to choose: do we cut off the fraction or round? Here we’ll show the fraction loss specifically, so you can see the difference.
fun main() {
val discount = kotlin.random.Random.nextDouble() * 30.0 // 0.0..30.0
val discountInt = discount.toInt() // cut off the fraction
println("discount = $discount") // discount = 12.345678...
println("discountInt = $discountInt%") // discountInt = 12%
}
If you want rounding “by the rules,” you now know you need to do it as a separate step.
7. Cheat-sheet diagram for conversions
Sometimes it’s useful to keep not individual methods in mind, but a small “route” for solving the problem. Here’s a simple flowchart that helps you not confuse “cutting off the fraction” with “rounding,” and also remember that characters live in the world of codes.
flowchart TD
A["We have a value"] --> B{Is it a number or a character?}
B -->|Number| C{Need a different numeric type?}
C -->|Yes| D["Explicitly: toDouble/toFloat/toInt"]
C -->|No| E["Leave as is"]
D --> F{Converting from Double to Int?}
F -->|Yes| G["Understand: the fraction will be lost"]
F -->|No| H["Usually safer"]
B -->|Char character| I["Take ch.code (Int)"]
I --> J{Want a digit?}
J -->|Yes| K["Check '0'..'9' and subtract '0'.code"]
J -->|No| L["Convert numbers ↔ characters via toChar()"]
8. Common mistakes
Mistake #1: confusing '7', "7", and 7.
These are three different types: Char, String, and Int. If you store a digit as a Char, it is not the number 7 and not the string "7". Trying to apply string methods to a Char, or expecting a Char to “become a number” on its own, is a common source of confusion and weird conversions.
Mistake #2: thinking toInt() rounds.
toInt() does not round; it discards the fractional part. This is especially annoying when you expect “a discount of 12.9% becomes 13%,” but you get 12%. Kotlin isn’t to blame here: it did exactly what you asked. If you need rounding math—round first, then convert to Int.
Mistake #3: doing Double → Int “somewhere later,” without thinking about meaning.
Sometimes people write, “Let it be a Double for now, and I’ll convert it to an Int at the end.” That’s risky: you can lose data at a point where it’s already too late to understand where the error came from. It’s better to keep an agreement: where you store the precise computation (Double), and where you specifically need an integer result (Int), and perform the conversion in a clear place.
Mistake #4: converting a digit from Char to Int without checking the range.
The expression ch.code - '0'.code is correct only if ch really is in the range '0'..'9'. If it turns out to be 'A', you’ll get some number (for example, 17), and the program won’t crash—it will just start doing nonsense. So: check first, then convert.
Mistake #5: forgetting that Char is a Unicode character, not “a letter from the ASCII table.”
In learning examples, we can safely use 'A'..'Z' and '0'..'9', and it works predictably. But it’s important to remember that Char in Kotlin is Unicode, and “letters” aren’t only Latin. That’s exactly why it’s more correct to think in terms of “character code” and specific ranges than in terms of “it’s a letter, so plus one must be the next one.”
GO TO FULL VERSION