Kotlin's type system is one of its most important design decisions. Statically typed means the type of every expression is known at compile time — before your code ever runs. This allows the compiler to catch a broad class of errors that would otherwise be discovered only at runtime, often in production.
Unlike dynamically typed languages where a variable can hold any type at any time, Kotlin variables have a fixed type that is determined at the point of declaration. The compiler uses this information to verify that you are using values correctly throughout your program.
The Variable Type System at a Glance
Variables
|
+--[mutable]-----> var ----+
| |
+--[immutable]---> val ----+----> Type
| |
+--[nullable]----> ? -----+
|
+----------+----------+
| | |
Integer Float Text
| | |
Byte Float Char
Short Double String
Int
Long
|
Bool
|
Boolean
var and val: Mutability in Kotlin
Every variable in Kotlin is declared with one of two keywords that determine whether the variable can be reassigned after its initial value is set.
var — Mutable Variable
var declares a mutable variable — its value can be changed after the initial assignment. Use var when the value needs to change over time.
var number = 17
println("number = $number") // number = 17
number = 18 // Reassignment is allowed
println("number = $number") // number = 18
val — Immutable Variable
val declares an immutable variable — once assigned, it cannot be reassigned. It is equivalent to a final variable in Java.
val number = 17
println("number = $number") // number = 17
number = 18 // Compiler error: val cannot be reassigned
Attempting to reassign a val variable is a compile-time error — the program will not compile. This is intentional: the compiler enforces the immutability contract.
Prefer `val` over `var` wherever possible. Immutability simplifies data flow, eliminates a class of bugs related to accidental mutation, and makes code easier to reason about — especially in concurrent scenarios.
Integer Types
Kotlin provides four integer types with different ranges, each mapping to a Java primitive type when targeting the JVM:
val byte_var: Byte = 127 // 8-bit (-128 to 127)
val short_var: Short = 32767 // 16-bit (-32768 to 32767)
val int_var: Int = 2147483647 // 32-bit (-2^31 to 2^31-1)
val long_var: Long = 9223372036854775807 // 64-bit (-2^63 to 2^63-1)
| Type | Size | Range |
|---|---|---|
Byte | 8-bit | -128 to 127 |
Short | 16-bit | -32,768 to 32,767 |
Int | 32-bit | -2,147,483,648 to 2,147,483,647 |
Long | 64-bit | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
In practice, Int is used for the vast majority of integer values in Android development. Use Long when you need to store values that exceed Int's maximum (timestamps in milliseconds, for example, routinely exceed Int range).
Floating-Point Types
Kotlin provides two floating-point types for representing decimal numbers:
val float_var: Float = 3.4028235e38f // 32-bit floating point
val double_var: Double = 1.7976931348623157e308 // 64-bit floating point
The e notation denotes scientific notation — 1e3 means 1 × 10³ = 1000.
An important detail: Kotlin infers Double as the type of decimal literals by default. To create a Float literal, you must append the f suffix:
val inferredDouble = 3.14 // Kotlin infers: Double
val explicitFloat = 3.14f // Float — the 'f' suffix is required
| Type | Size | Precision |
|---|---|---|
Float | 32-bit | ~6-7 significant decimal digits |
Double | 64-bit | ~15-16 significant decimal digits |
Double is the default and should be preferred unless you have a specific reason to use Float (such as memory constraints or interfacing with APIs that require it).
Text Types
Kotlin has two text types — one for a single character and one for a sequence of characters:
val character_var: Char = '#' // Single character
val text_var: String = "Learning Kotlin" // Text string
Kotlin String supports string templates — embedding variables and expressions directly inside string literals with the $ prefix:
val name = "Alice"
val age = 30
// Simple variable interpolation
println("Hello, $name!")
// Expression interpolation with ${}
println("In 10 years, $name will be ${age + 10} years old.")
This eliminates string concatenation and makes string construction far more readable.
Boolean Type
Booleans represent truth values:
val yes_var: Boolean = true
val no_var: Boolean = false
Booleans are used extensively in conditionals, flags, and state representation. In Kotlin, Boolean is a proper type — it cannot be treated as an integer (as is sometimes done in C or older Java patterns).
Type Inference
Type inference is a compiler feature that allows you to omit explicit type annotations when the compiler can determine the type from context. Kotlin's compiler is capable of inferring the type of most variables.
val name = "Alice" // Compiler infers: String
val count = 42 // Compiler infers: Int
val ratio = 3.14 // Compiler infers: Double
val active = true // Compiler infers: Boolean
These are fully, statically typed variables — the types exist and are enforced by the compiler. You simply do not have to write them because the compiler can see what you mean.
That said, there are situations where adding an explicit type annotation is worth the extra characters:
- Code readability — when the inferred type is not immediately obvious from the right-hand side value, the explicit type makes the code self-documenting
- Programming against an interface — when you want to declare a variable as an interface type even though you are assigning a concrete implementation
// The concrete type (ArrayList) is inferred, but declaring List is intentional
val names: List<String> = ArrayList()
// Makes it explicit that this is a Long, not an Int
val fileSize: Long = 1024
Type inference is a tool for reducing noise — not a reason to hide types that matter for clarity.
Nullable Types
By default, all Kotlin types are non-nullable. A variable of type String is guaranteed to never hold a null value — this is enforced by the compiler, not just a convention.
val input: String = null // Compiler error: cannot assign null to non-nullable type
This design choice is deliberate and important. The majority of null pointer exceptions in Java happen because developers forget that a variable might be null. Kotlin makes you make a conscious, explicit choice when you want nullable behavior.
Declaring a Nullable Type
To allow a variable to hold null, you append ? to the type:
val input: String? = null // Valid — String? is explicitly nullable
The ? is a part of the type. String and String? are two distinct types in Kotlin's type system.
The Null Safety Problem
Having declared a nullable variable, the compiler will now protect you from calling methods on it without checking for null first:
val input: String? = null
val output = input.toUpperCase() // Compiler error: unsafe call on nullable type
The compiler refuses to compile this code because calling toUpperCase() on a null value would throw a NullPointerException. You must explicitly handle the null case.
Null Handling Operators
Kotlin provides three operators for working with nullable types, each with a different risk profile.
Safe Call Operator `?.`
The safe call operator ?. calls a method on a nullable object but returns null instead of throwing an exception if the object is null.
val input: String? = null
val output = input?.toUpperCase() // Returns null safely — no crash
If input is null, output is null. If input has a value, toUpperCase() is called normally.
For nested nullable objects, chain the safe call operator:
val city = user?.address?.city // Returns null if user or address is null
Elvis Operator `?:`
The Elvis operator ?: provides a default value when the left-hand side is null. If the left side is not null, it returns the left side; if it is null, it returns the right side.
val name: String? = null
val chatName = name ?: "Anonymous" // chatName = "Anonymous"
val displayName = chatName.toUpperCase() // "ANONYMOUS" — safe, chatName is non-null
The name "Elvis operator" comes from the visual resemblance to the Elvis emoji: ?:-O.
The Elvis operator can also throw exceptions or perform other actions:
val input: String? = null
val userInput = input ?: throw IllegalArgumentException("Input must not be null.")
This is a clean way to enforce preconditions at the start of a function.
Non-Null Assertion Operator `!!`
The non-null assertion operator !! tells the compiler: "I know this cannot be null here — trust me." It bypasses the null safety check entirely and throws a NullPointerException if the value actually is null.
val input: String? = null
val output = input!!.toUpperCase() // NullPointerException at runtime if input is null
Use this operator sparingly. It is essentially a declaration that you are taking full responsibility for null safety at this specific point. If you are wrong, you get an NPE — the very thing Kotlin's type system was designed to prevent.
Legitimate uses of !! are rare: typically only when you have external guarantees about non-nullability that the compiler cannot verify (such as values initialized by dependency injection frameworks).
Summary
| Concept | Keyword / Syntax | Description |
|---|---|---|
| Mutable variable | var | Value can be reassigned |
| Immutable variable | val | Value cannot be reassigned after initialization |
| Nullable type | Type? | May hold null; compiler enforces null handling |
| Safe call | ?. | Call method; return null if receiver is null |
| Elvis operator | ?: | Provide default if left side is null |
| Non-null assertion | !! | Assert non-null; throws NPE if wrong |
Kotlin's type system is not just a set of rules — it is a set of guarantees. When the compiler says a value is non-null, it truly cannot be null. When it says a type is Int, it truly is an integer. This predictability is what makes Kotlin programs more robust and easier to reason about than their Java equivalents.
Prefer val over var. Prefer non-nullable over nullable. Use !! only when you truly have no alternative.