Basic Types

Every value in a Kotlin program has a type: whole numbers, decimal numbers, true/false flags, single characters, and text all have their own dedicated type. Kotlin’s basic types look similar to Java’s primitives at first glance, but they behave differently in important ways — they are full objects with member functions, they never convert into each other implicitly, and every one of them is non-null by default. Understanding these types precisely is the foundation for everything else you’ll write in Kotlin, from simple arithmetic to null-safe APIs.

Overview: How Kotlin’s Type System Works

In Java, int, double, and boolean are primitive types that live separately from their boxed wrapper classes (Integer, Double, Boolean). Kotlin removes that split at the language level: Int, Double, and Boolean are ordinary types with methods you can call on them, like 5.toString() or 3.14.roundToInt(). Under the hood, the Kotlin compiler still emits efficient JVM primitives (int, double, boolean) wherever it safely can — it only boxes a value into an object (using classes like java.lang.Integer) when it must, for example when the type is nullable (Int?) or stored in a generic collection like List<Int>. You get object-oriented convenience without paying a performance tax in the common case.

Every basic type also comes with a nullable counterpart, written with a trailing ?: Int can never hold null, but Int? can. This split is the basis of Kotlin’s compile-time null safety, covered in depth in its own lesson — for now, just know that every type shown here is non-null unless you explicitly add ?.

Kotlin is also strongly and statically typed with excellent type inference. You rarely need to write out a type explicitly, because the compiler figures it out from the value on the right-hand side of =. val age = 30 infers Int; val price = 19.99 infers Double. Crucially, once a type is inferred or declared, it is fixed — there is no automatic (“implicit”) widening between numeric types the way Java silently promotes an int to a long. Every numeric conversion in Kotlin must be written out explicitly, which the compiler enforces and which prevents a whole class of silent precision-loss bugs.

Syntax

The general form for declaring a typed value is:

val name: Type = value
var name: Type = value   // only when the value needs to change
val name = value          // type inferred from value
Type Size Range / Notes
Byte 8-bit -128 to 127
Short 16-bit -32768 to 32767
Int 32-bit -2147483648 to 2147483647; the default type for whole-number literals
Long 64-bit Roughly ±9.2 quintillion; literals need an L suffix, e.g. 10_000_000_000L
Float 32-bit ~6–7 significant decimal digits; literals need an f suffix, e.g. 3.14f
Double 64-bit ~15–16 significant decimal digits; the default type for decimal literals
Boolean logical true or false, no other values allowed
Char 16-bit A single UTF-16 character, written with single quotes: 'A'
String An immutable sequence of Char, written with double quotes: "text"

Large numeric literals can use underscores as visual separators, which the compiler simply ignores: 1_000_000 means the same thing as 1000000.

Examples

Example 1: Declaring values with inferred and explicit types

fun main() {
    val age: Int = 30
    val price = 19.99          // inferred as Double
    val isActive = true        // inferred as Boolean
    val grade: Char = 'A'
    val name = "Kotlin"        // inferred as String

    println("age = $age (Int)")
    println("price = $price (Double)")
    println("isActive = $isActive (Boolean)")
    println("grade = $grade (Char)")
    println("name = $name (String)")
}

Output:

age = 30 (Int)
price = 19.99 (Double)
isActive = true (Boolean)
grade = A (Char)
name = Kotlin (String)

Two values here use an explicit type annotation (age: Int and grade: Char), while the rest let the compiler infer the type from the literal on the right. Both styles produce the exact same compiled code — the annotation is purely for the compiler (and for readers), it has no runtime cost.

Example 2: Explicit numeric conversions

fun main() {
    val smallNumber: Int = 42
    val bigNumber: Long = smallNumber.toLong()
    val asDouble: Double = smallNumber.toDouble()
    val asByte: Byte = smallNumber.toByte()

    println("Int: $smallNumber")
    println("Long: $bigNumber")
    println("Double: $asDouble")
    println("Byte: $asByte")

    val a = 10
    val b = 3
    println("Integer division: ${a / b}")
    println("Double division: ${a.toDouble() / b}")
}

Output:

Int: 42
Long: 42
Double: 42.0
Byte: 42
Integer division: 3
Double division: 3.3333333333333335

Every numeric type provides conversion functions like toLong(), toDouble(), and toByte() — there is no automatic widening, so you always convert explicitly. Notice also the classic division gotcha: a / b with two Int values performs integer division and truncates to 3, while converting one operand to Double first produces the precise result 3.3333333333333335.

Example 3: Char and String basics

fun main() {
    val initial: Char = 'K'
    val language = "Kotlin"
    val version = 2.0

    println("Language: $language, version $version")
    println("First letter: $initial")
    println("Length of name: ${language.length}")
    println("Uppercase: ${language.uppercase()}")

    val multiline = """
        Line one
        Line two
    """.trimIndent()
    println(multiline)
}

Output:

Language: Kotlin, version 2.0
First letter: K
Length of name: 6
Uppercase: KOTLIN
Line one
Line two

String templates ($name and ${expression}) let you interpolate values directly into a string without concatenation. Triple-quoted strings ("""...""") preserve line breaks for multi-line text, and trimIndent() strips the common leading whitespace so the output isn’t indented to match your source code.

How It Works Step by Step

When the compiler processes a line like val price = 19.99, it first evaluates the literal 19.99 and determines its type from context: a decimal literal defaults to Double unless it carries an f/F suffix (which makes it Float) or the target type is already known to be Float from a declared type annotation. Whole-number literals default to Int, are promoted to Long automatically only if they don’t fit in 32 bits, or become Long if they carry an L suffix or the target type calls for it. Once that type is fixed, it is baked into the variable for its entire lifetime — there is no step at which Kotlin will later coerce an Int into a Long for you; every later use that needs a different type must call an explicit conversion function such as .toLong(). This single design decision is why Kotlin code almost never suffers the silent precision-loss bugs that plague languages with implicit numeric coercion.

Common Mistakes

Mistake 1: Assuming Int converts to Long automatically

val i: Int = 42
val l: Long = i   // Type mismatch: inferred type is Int but Long was expected

Unlike Java, Kotlin never widens an Int into a Long implicitly, even though every Int value fits inside a Long. You must convert explicitly:

val i: Int = 42
val l: Long = i.toLong()

Mistake 2: Not expecting Int to overflow silently

fun main() {
    val max = Int.MAX_VALUE
    val overflowed = max + 1
    println(overflowed)
}

Output:

-2147483648

The compiler does not catch integer overflow — arithmetic on Int simply wraps around to the minimum value once it exceeds the maximum, exactly like Java’s int. If a computation might exceed roughly 2.1 billion, use Long from the start rather than discovering the wraparound at runtime.

Mistake 3: Mixing up Char and String quoting

val letter: Char = "A"   // error: a String literal cannot be assigned to Char

Char literals always use single quotes and hold exactly one character; String literals always use double quotes. The two are not interchangeable, even for one-character text:

val letter: Char = 'A'

Best Practices

  • Prefer val over var for every basic-type value unless it genuinely needs to be reassigned.
  • Let the compiler infer obvious types (val count = 0); add an explicit type annotation when it improves clarity for a reader, such as on public function signatures or class properties.
  • Default to Int for whole numbers and Double for decimals — only reach for Long, Short, Byte, or Float when you have a specific reason (range, memory footprint, or interop requirement).
  • Use underscores in long numeric literals for readability: 1_000_000 instead of 1000000.
  • Always convert numeric types explicitly with functions like toInt(), toDouble(), and toLong() — never rely on implicit coercion, because Kotlin doesn’t have any.
  • Watch for integer overflow in arithmetic on Int; switch to Long proactively if a sum or product could exceed roughly 2.1 billion.
  • Use string templates ("$name", "${expr}") instead of string concatenation with + for both clarity and performance.

Practice Exercises

  1. Write a program that declares a Float and a Double both holding the value 1f / 3f and 1.0 / 3.0 respectively, then prints both. Compare how many digits of precision each one shows.
  2. Declare a Double equal to 9.99, convert it to Int with .toInt(), and print the result. Predict the output before you run it — does it round or truncate?
  3. Write a program that computes the average of three Int exam scores (85, 90, 78) as a Double, avoiding the integer-division bug shown in Example 2. Expected output: 84.33333333333333.

Summary

  • Kotlin’s basic types — Byte, Short, Int, Long, Float, Double, Boolean, Char, and String — are full objects, not bare primitives, though the compiler still uses efficient JVM primitives under the hood when possible.
  • Every basic type is non-null by default; a trailing ? (like Int?) is required to allow null.
  • Type inference fills in the type from a literal or expression, but once fixed, a type never changes on its own — there is no implicit numeric widening in Kotlin.
  • Numeric conversions must be explicit, using functions like toInt(), toLong(), and toDouble().
  • Int arithmetic can silently overflow and wrap around; use Long when values might exceed the 32-bit range.
  • Char uses single quotes for exactly one character; String uses double quotes (or triple quotes for multi-line text) for text of any length.