---
title: 'Basic Types'
source: 'https://academia.sh/en/courses/kotlin/basic-types'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes the roles of the `Int`, `Long`, `Double`, `Char`, and `Boolean` types'
    - 'Shows that conversion between number types is written explicitly and that comparison does not match the types'
    - 'Explains why boxed numbers compared with `===` come out inconsistent'
    - 'States that the value `Double.NaN` gives different equality depending on the static type'
---

# Basic Types

Introduces the number types, the character, and the logical value; measures that the criterion of equality changes once a nullable `Int` is boxed.

In the previous lesson we saw that the type inferred from the constant `7` is `Int`. Now we look at the basic types: `Int` and `Long` for integers, `Double` for floating-point numbers, `Char` for a single character, and `Boolean` for the logical value. The question of equality has two faces for these types. First, can numbers of different types be compared with `==`? Second, when a number is boxed, can the same value split into two objects?

## Types and Explicit Conversion

In Kotlin, an integer constant is an `Int` by default. A constant that exceeds the `Int` limit becomes a `Long` on its own; the `L` suffix is written to make a small value a `Long` as well. A constant with a decimal point is a `Double`. A `Char` is written in single quotes and gives its numeric code with `.code`. Conversions are explicit: an `Int` is not assigned to a `Long` variable automatically; you write `count.toLong()`. Arithmetic, on the other hand, can mix types: when an `Int` and a `Double` are added, the result is a `Double`.

In this lesson too, the measurements are made with the helper file introduced in the first lesson. The `Criterion` class takes a value, its twin, and a different value, and asks three questions; the file is given below exactly as it was.

```kotlin
// Criterion.kt — classifies the criterion that makes two values equal
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
    val sameObject = first === twin
    val equalTwin = first == twin
    val equalOther = first == other

    val name: String = when {
        sameObject -> "singleton"
        !equalTwin -> "identity"
        !equalOther -> "content"
        else -> "partial-content"
    }

    fun report() {
        println("$label: same-object=$sameObject, twin=$equalTwin, other=$equalOther -> $name")
    }
}

fun summarize(checks: List<Criterion>) {
    for (name in listOf("singleton", "identity", "content", "partial-content")) {
        println("$name: ${checks.count { it.name == name }}")
    }
}
```

The program below runs all of these rules together. The `total` variable converts an `Int` value to a `Long` and multiplies it by a billion. `7 / 2` and `7 / 2.0` compare two divisions: in the first both sides are integers, in the second one side has a decimal point. `letter + 1` adds a number to a `Char`.

```kotlin
// Main.kt — numbers, character, logical value, and explicit conversion
fun main() {
    val count: Int = 3
    val total: Long = count.toLong() * 1_000_000_000L
    val size = 2.5
    val letter = 'K'
    val ready = count > 2
    println(count + size)
    println(total)
    println(7 / 2)
    println(7 / 2.0)
    println(letter.code)
    println(letter + 1)
    println(ready)
}
```

```
5.5
3000000000
3
3.5
75
L
true
```

`count + size` gives 5.5: an `Int` and a `Double` were added and the result became a `Double`. `total` prints 3000000000; because this value exceeds the `Int` limit, a `Long` was needed. `7 / 2` gives three, because both sides are integers, so the division is carried out as an integer division and the fractional part is discarded. `7 / 2.0`, on the other hand, gives 3.5. `letter.code` prints 75, the code of the capital letter `K`. `letter + 1` gives the letter `L`, the next character. The last line shows that the `ready` variable is `true`.

**LB16.** In this section the division was measured with the constants `7` and `2`; the behavior of division with a negative number is measured in a separate section.

**LB17.** `Char` arithmetic was measured only with `letter + 1`; the type of the difference between two `Char` values was not measured in this lesson.

## Conversion Is Explicit, and So Is Comparison

That arithmetic can mix types does not hold for assignment and comparison. Assigning an `Int` value to a variable of type `Long` is rejected by the compiler; we saw this by compiling in the sandbox, so the fragment is without output.

```kotlin
val count: Int = 3
val wide: Long = count
```

The compiler reports that it found an `Int` where a `Long` was expected. Comparison follows the same rule: comparing an `Int` with a `Long` constant by `==` again does not compile.

```kotlin
val count: Int = 1
println(count == 1L)
```

The compiler reports that the `==` operator cannot be applied between `Int` and `Long`. The solution is to write the conversion explicitly: `count.toLong() == 1L`. But if the types are held as `Any`, the situation changes: the compiler cannot know the static type, so it asks for equality of the run-time objects.

```kotlin
// Main.kt — numbers held as Any
fun main() {
    val a: Any = 1
    val b: Any = 1L
    println(a == b)
}
```

```
false
```

`1` and `1L` did not come out equal when held as `Any`: the output is `false`. An `Int` object and a `Long` object belong to different classes even if their numeric values are the same, and `equals` takes this difference into account. So the comparison that the compiler rejected is not rejected once its type is hidden, but its answer is still `false`.

**LB18.** The compiler errors were obtained with Kotlin 2.4.21; the message wording may change in later versions, but what is measured in this lesson is the rule that the two comparisons are rejected.

## Boxed Numbers and NaN

The measurement of the lesson looks at two things. First, `Int?`, a nullable type, holds a boxed number; on the JVM this is an `Integer` object. Are two boxed numbers the same object even when they have the same value? Second, `Double.NaN` (the not-a-number value) gives different equality depending on the static type. First the comparison in which both sides are `Double` is printed, then the comparison in which the same value is held as `Any`.

```kotlin
// Main.kt — boxed numbers and NaN
fun main() {
    val boxedBig: Int? = 1000
    val boxedBigTwin: Int? = 1000
    val boxedSmall: Int? = 127
    val boxedSmallTwin: Int? = 127
    val nan = Double.NaN
    val anyNan: Any = nan
    val anyNanTwin: Any = Double.NaN
    println(nan == nan)
    println(anyNan == anyNan)
    val checks = listOf(
        Criterion("boxed-1000", boxedBig, boxedBigTwin, 1001),
        Criterion("boxed-127", boxedSmall, boxedSmallTwin, 128),
        Criterion("nan-as-any", anyNan, anyNanTwin, 1.0),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
```

```
false
true
boxed-1000: same-object=false, twin=true, other=false -> content
boxed-127: same-object=true, twin=true, other=false -> singleton
nan-as-any: same-object=false, twin=true, other=false -> content
singleton: 1
identity: 0
content: 2
partial-content: 0
```

The first two lines are NaN equality. `nan == nan`, because the static type is `Double`, follows the IEEE rule and gives `false`; NaN is not equal to itself. `anyNan == anyNan` gives `true`, because when the static type is `Any`, `==` calls the object's `equals` method, and `equals` treats NaN as equal to itself. The same number gives two different answers depending on the type it is held in.

Among the three measurement lines, `boxed-1000` is content: the two `Int?` variables have the value 1000 but are separate objects, `same-object=false`. `boxed-127` is singleton: the same value points to the same object on the JVM, `same-object=true`. Two numbers with the same type and the same form of value fell under different criteria according to the value. `nan-as-any` is content: with the twin `==` gives `true`, and they are separate objects.

**LB19.** 127 and 1000 were measured on the JVM, with Kotlin 2.4.21; no general limit on which values are shared is drawn in this lesson, and no claim is made for values other than 127 and 1000.

**LB20.** The NaN observation was measured with a single `Double.NaN` value held as `Any`; that `Float.NaN` gives the same result was not measured in this lesson.

## Overflow, Division by Zero, and Decimal Error

That a type has a limit calls for knowing what happens when the limit is exceeded. The block below shows three limit cases together: a sum that exceeds the `Int` limit, integer division by zero, and decimal division by zero. It then tests the error that decimal numbers inherit from binary representation with `0.1 + 0.2`. The `Math.addExact` call, which catches overflow as an error, is a method of the JVM itself; Kotlin can call it directly.

```kotlin
// Main.kt — overflow, division by zero, and decimal error
fun main() {
    val zero = 0
    val big = 3_000_000_000
    println(Int.MAX_VALUE + 1)
    println(Long.MAX_VALUE)
    println(big::class.simpleName)
    try {
        Math.addExact(Int.MAX_VALUE, 1)
    } catch (e: ArithmeticException) {
        println(e.message)
    }
    try {
        println(1 / zero)
    } catch (e: ArithmeticException) {
        println(e.message)
    }
    println(1.0 / 0.0)
    println(0.1 + 0.2)
    println(0.1 + 0.2 == 0.3)
}
```

```
-2147483648
9223372036854775807
Long
integer overflow
/ by zero
Infinity
0.30000000000000004
false
```

`Int.MAX_VALUE + 1` does not raise an error; it silently becomes `-2147483648`: the number wraps from the largest value to the smallest. For `Long.MAX_VALUE`, `9223372036854775807` is printed. Because the constant `3_000_000_000` exceeds the `Int` limit, type inference makes it a `Long` by itself; the `Long` in the output confirms this. `Math.addExact`, on the other hand, throws an error with the message `integer overflow` for the same sum; so if you want overflow to be noticed, you choose this path.

When an integer is divided by zero, the message `/ by zero` comes. In decimal division the same operation raises no error and returns `Infinity`. The last two lines show the limit of the decimal number: the result of `0.1 + 0.2` is `0.30000000000000004`, and the `==` comparison with `0.3` gives `false`.

The common mistake appears in two places. The first is forgetting that overflow raises no error and accumulating a sum in an `Int`. The second is comparing two decimal numbers with `==`; for values that must come out exact, such as money, a decimal type is the wrong choice. Java comparison: these outputs are the same as in Java, because Kotlin uses the JVM's own arithmetic here; Kotlin does not require a separate type to be written for this behavior. This sameness is stated on the basis of the run-time environment being the same, not by running Java code.

**LB21.** Overflow was measured with an `Int` sum, and division by zero with `Int` and `Double`; that `Long` overflow and `Float` give the result in the same form was not measured in this lesson.

## Narrowing Conversions

A widening conversion causes no trouble; the trouble appears where a value is fitted into a narrower type. The block below measures five conversions and the conversion from text to number: converting `300` to `Byte`, decimal numbers to `Int`, `65` to `Char`, and a valid or an invalid string to `Int`.

```kotlin
// Main.kt — narrowing conversions and number from text
fun main() {
    println(300.toByte())
    println(3.99.toInt())
    println((-3.99).toInt())
    println(65.toChar())
    println("42".toInt() + 1)
    println("x".toIntOrNull())
    try {
        "x".toInt()
    } catch (e: NumberFormatException) {
        println(e.message)
    }
}
```

```
44
3
-3
A
43
null
For input string: "x"
```

`300.toByte()` cannot carry more than eight bits and becomes `44`: only the low eight bits remain, the upper bits are discarded, and no error is raised. `3.99.toInt()` does not round; it truncates the fractional part, and `3` comes out. For a negative value, `(-3.99).toInt()` also gives `-3`, so there is truncation toward zero, not rounding down. `65.toChar()` gives the capital letter `A`; the bridge between a number and a character code is explicit.

There are two ways to convert text to a number. `"42".toInt() + 1` gives `43` on valid text. On invalid text the two options part ways: `toIntOrNull()` returns `null` instead of throwing an error, while `toInt()` throws a `NumberFormatException` with the message `For input string: "x"`. For text that comes from a user, the first way asks for a `null` check but does not bring the program down, while the second way does.

The reason for the mechanism is that conversions are written explicitly. The compiler does not assign silently to a narrow type; writing `toByte()` says that you knowingly accept the loss of information. In Java, the same narrowing is written with a cast of the form `(byte) 300`, and it is often overlooked.

**LB22.** The conversions were measured with `Byte`, `Int`, and `Char`; that the same truncation rules hold for `Short`, `Float`, and `Long` was not measured in this lesson.

## Division and Modulo with Negative Numbers

In the first section we saw that `7 / 2` discards the fractional part. But what if the dividend is negative? The block below shows four ways of writing a negative division and remainder operation, then three other limit cases together: the absolute value of the smallest `Int`, hexadecimal and binary constant notation, and ordering between two `Char` values.

```kotlin
// Main.kt — division and modulo with negative numbers
import kotlin.math.abs

fun main() {
    println(-7 / 2)
    println(-7 % 2)
    println((-7).mod(2))
    println(Math.floorMod(-7, 2))
    println(abs(Int.MIN_VALUE))
    println(0xFF)
    println(0b1010)
    println('a' < 'b')
}
```

```
-3
-1
1
1
-2147483648
255
10
true
```

`-7 / 2` gives `-3`; the fractional part is discarded toward zero, not downward. `-7 % 2`, on the other hand, gives `-1`: the sign of the remainder follows the sign of the dividend. If you want the remainder always to come out zero or positive, you need `(-7).mod(2)` or the JVM's `Math.floorMod(-7, 2)` call; both gave `1`. A common mistake is forgetting that a negative remainder can come out when converting the index of an array with `%`.

`abs(Int.MIN_VALUE)` gives `-2147483648`; even the absolute value does not come out positive, because `+2147483648` has no counterpart in `Int`. The constant `0xFF` prints `255`, and the constant `0b1010` prints `10`; writing a constant in hexadecimal or binary does not change the type itself, only how it is written. The expression `'a' < 'b'` gives `true`, because `Char` values are ordered by character code.

Java comparison: the `%` operator in Java also follows the sign of the dividend. The `mod` that Kotlin adds offers, as a method next to the operator, the result obtained with `Math.floorMod` in Java. Both of these results were observed in the output above.

**LB23.** Negative division and remainder were measured with a single pair (`-7`, `2`); that the same sign rule holds for a negative divisor or for the `Long` type was not measured in this lesson.

## Habits That Come from the Measurements

In numeric types, Kotlin's guarantees are limited, and this lesson's measurements showed where the limit begins. Overflow is silent: the output of `Int.MAX_VALUE + 1` was `-2147483648` and no exception was thrown. This behavior is the same as in Java. Where overflow can be harmful, for example in a money calculation or in the sum of durations, you need to use `Long` or to write checked operations such as `Math.addExact`. For a `Long`, the same overflow lies at a much more distant limit and is enough in most cases, but it does not remove the limit.

Division by zero throws an exception for integers (`/ by zero`), while for decimal numbers it gives `Infinity`. This difference means that when the type of a calculation is changed, the error behavior changes too. A piece of code that uses `Double` quietly keeps running when divided by zero and spreads the result; to notice the error, an `isInfinite` or `isNaN` check is needed. Because decimal numbers are kept in base two, the output of `0.1 + 0.2` was `0.30000000000000004` and the `== 0.3` comparison gave `false`. Where an exact value is wanted, such as money, `BigDecimal` or an integer converted to the smallest currency unit is used.

Narrowing conversions also call for attention. Converting from a larger type to a smaller one truncates or wraps the number, and results such as `44`, `3`, `-3`, and `A` in the measurement showed that this is not always "rounding": `toInt` discards the decimal part, so `-3.99` becomes `-3`. `toIntOrNull` returns `null` for a string that cannot be converted, while `toInt` throws an exception in the same case, and the message `For input string: "x"` confirmed this behavior. When converting text from a user to a number, choosing the `OrNull` form makes the error a part of the flow.

Finally, the negative division and modulo measurement showed that the sign of the result follows the same rule as in Java: division rounds toward zero, and the remainder carries the sign of the dividend. The modulo in mathematics and the modulo operation in a program are not the same thing; in a circular index calculation that works with negative numbers, code that uses `%` instead of `mod` can push the index below zero.

## The Bounding Measurement

For a boxed number, the criterion depends on the value: 1000 is content, 127 is singleton. For this reason, comparing the identity of numbers with `===` is unreliable; number comparison is done with `==`. The same value came out singleton at 127 and content at 1000. Instead of a single criterion, we saw two criteria.

NaN sets a separate limit: in a comparison whose static type is `Double`, NaN is not equal, and under the type `Any` it is equal. The meaning of the `==` operator changes at the point where the operator depends on the static type of the expression on the left.

## Summary

- An integer constant is an `Int`; a value that exceeds the limit asks for a `Long`. A constant with a decimal point is a `Double`.
- An `Int` value is not assigned to a `Long` variable automatically, and an `Int` and a `Long` constant cannot be compared with `==`.
- Arithmetic can mix types: the sum of an `Int` and a `Double` is a `Double`.
- A nullable `Int` is boxed; 127 came out singleton and 1000 came out content.
- This lesson's criterion: **content** (1000); bounding measurement: the value 127 is a singleton on the JVM, and NaN gives different equality depending on the static type.

## Next Step

We have seen the numbers and the character. In the next lesson we return to strings: templates, raw strings, and `trimIndent`. We will measure whether building a string with a template gives the same object as writing the same text by hand.
