Basics & Types
Variables, constants, type inference, strings, numbers, and basic operators. Learn how Kotlin's syntax differs from JS and Python.
By the end of this chapter, you will:
- Understand the difference between
valandvar - Know Kotlin's basic types and how type inference works
- Use string templates and multi-line strings
- Work with basic operators and expressions
- Understand why Kotlin has no primitive types
Variables: val vs var
In Kotlin, every variable is declared with either val or var. This choice is important because Kotlin strongly encourages immutability.
Theory
Kotlin's property system is designed around the principle that read-only data is safer. A val (value) can only be assigned once and behaves like a final variable in Java. Under the hood, the compiler generates a private field with a getter but no setter. A var (variable) generates both a getter and a setter, making it mutable. This distinction exists at the language level — the compiler enforces it, not just a convention.
Immutability by default eliminates entire categories of bugs: race conditions in multi-threaded code, accidental state mutations, and unintended side effects. When you see val in Kotlin code, you know that reference will never point to a different object.
val name = "Alex" // Immutable — like JS `const` or Python doesn't have this
var age = 25 // Mutable — like JS `let` or Python variable
name = "Sam" // ❌ ERROR: Val cannot be reassigned
age = 26 // ✅ OK
val. Only use var when you absolutely need to change the value. This prevents an entire class of bugs.
Type Inference
The Kotlin compiler performs local type inference by examining the initializer expression. This is not dynamic typing — the type is fixed at compile time. Explicit type declarations are required when there is no initializer (e.g., val name: String) or when you want a wider type than the initializer suggests (e.g., val n: Number = 42).
val inferred = "Hello" // Compiler knows this is String
val explicit: String = "Hello" // You can specify the type after a colon
val count = 42 // Int
val price = 19.99 // Double
val isActive = true // Boolean
Compare with JS / Python
const name = "Alex";
let age = 25;
// No type annotations in JS
// typeof name === "string"
name = "Alex" # no const
age = 25
# Type hints exist but are optional
name: str = "Alex"
Kotlin difference: val is the default mindset. Type inference is automatic but the type is fixed at compile time — no runtime type changes.
Basic Types
Unlike Java, Kotlin has no primitive types at the language level — Int, Double, Boolean, etc. are all objects. The compiler performs autoboxing optimizations: when possible, it compiles these to JVM primitives (int, double, boolean) for performance, but you write them as objects. This means you can call methods on numbers: (-5).absoluteValue or 42.toString().
// Everything is an object — methods available on literals
val abs = (-10).absoluteValue // 10
val hex = 255.toString(16) // "ff"
val bits = 0b1010.countOneBits() // 2 (Kotlin 1.4+)
// JVM primitives under the hood, but you never think about it
val efficient: Int = 1_000_000 // compiled to primitive int
| Kotlin Type | Description | Example |
|---|---|---|
Int | 32-bit integer | 42 |
Long | 64-bit integer | 42L |
Double | 64-bit floating point | 3.14 |
Float | 32-bit floating point | 3.14f |
Boolean | true or false | true |
Char | Single character | 'A' |
String | Text | "Hello" |
Int, Double, etc. are all objects. The compiler optimizes them to JVM primitives when possible, but you don't need to think about it.
📖 Official docs: Basic Types (kotlinlang.org)
Number Literals
Kotlin supports multiple number bases and lets you use underscores for readability. This is especially useful for bitmasks, color values, and large constants.
val binary = 0b00001011 // 11 in decimal — useful for flags/bitmasks
val hex = 0x0F // 15 in decimal — common for color channels
val large = 1_000_000 // One million — underscores for readability
// Real-world: bitmask flags and hex values
val permissionFlags = 0b00001101 // bitwise permission flags
val colorChannel = 0xFF // 255 — a single RGB channel
1_000_000 is the same as 1000000.
0xFF121212 exceed Int's max value (2,147,483,647) and become Long in Kotlin. Use Color.parseColor("#FF121212") or the Android Color class for full ARGB values instead of raw hex literals.
Strings
Strings in Kotlin are more powerful than in JS or Python, especially with templates and multi-line support.
Theory
Kotlin strings are immutable sequences of UTF-16 characters (backed by java.lang.String on the JVM). The compiler transforms string templates into StringBuilder operations at compile time — there is no runtime interpolation engine. This means "Hello, $name" is as efficient as manual concatenation. Multi-line strings (triple quotes) are called raw strings and do not support \ escaping — what you type is what you get, including line breaks and indentation.
String Templates
val name = "Alex"
val age = 25
// Simple variable
val greeting = "Hello, $name!" // "Hello, Alex!"
// Expression in braces
val nextYear = "You'll be ${age + 1}" // "You'll be 26"
// Calling functions in templates
val shout = "HELLO, ${name.uppercase()}!" // "HELLO, ALEX!"
// Escaping $ when you need a literal dollar sign
val price = "The price is \$50" // "The price is $50"
// Raw strings (no escaping needed)
val path = "C:\\Users\\Alex" // regular string — need escaping
val rawPath = """C:\Users\Alex""" // raw string — no escaping!
"https://api.example.com/users/${userId.trim()}". Escaping $ is essential when displaying prices or currency: "Total: \$${totalAmount}".
Multi-line Strings
Raw strings preserve all whitespace exactly as typed. Use trimIndent() to remove common leading whitespace (useful for aligning with code indentation) or trimMargin("|") to strip everything up to a margin prefix character.
val json = """
{
"name": "$name",
"age": $age
}
""".trimIndent() // Removes leading whitespace from each line
Compare with JS / Python
const name = "Alex";
const greeting = `Hello, ${name}!`; // template literal
const raw = `Line 1
Line 2`; // multi-line
name = "Alex"
greeting = f"Hello, {name}!" # f-string
raw = """Line 1
Line 2""" # triple quotes
Kotlin difference: Templates work in regular strings too — no special ` or f prefix needed. Raw strings use triple quotes and preserve formatting.
Type Conversion
Kotlin does not do implicit type conversion. You must be explicit.
Theory
Java allows implicit widening conversions (int → long → float → double) which can silently lose precision (e.g., long to float). Kotlin rejects this design — every conversion must be explicit via .toXxx() methods. This prevents subtle precision bugs. The toInt(), toDouble(), etc. methods are defined as member functions on number types, not as external utility functions.
val i = 10
val d = i.toDouble() // ✅ 10.0
// val bad: Double = i // ❌ ERROR: Type mismatch
val s = "42"
val num = s.toInt() // ✅ 42
// val bad2 = "abc".toInt() // ❌ Runtime exception!
"5" + 3 is not "53" in Kotlin — it's a compile error. Kotlin won't silently convert types for you.
📖 Official docs: Explicit Number Conversions (kotlinlang.org)
Late Initialization
Sometimes you need a non-nullable variable that can't be initialized immediately — for example, in dependency injection or test setup where the value is assigned after object creation.
Theory
lateinit is a Kotlin compiler feature that delays initialization checking to runtime. The compiler generates a special nullable backing field but exposes it as a non-nullable type. Accessing the property before initialization throws UninitializedPropertyAccessException. You can check initialization status via the property reference syntax ::property.isInitialized. This exists primarily for frameworks (Android, Spring, test frameworks) where initialization happens through lifecycle methods or dependency injection containers outside the class constructor.
class UserRepository {
lateinit var database: Database // Will be assigned later
fun setup(db: Database) {
database = db
}
fun fetchUser(id: String): User {
// Must check initialization before use
if (::database.isInitialized) {
return database.query(id)
} else {
throw IllegalStateException("Database not initialized")
}
}
}
// Common in Android — views are initialized after layout inflation
class MainActivity {
lateinit var submitButton: Button
fun onCreate() {
submitButton = findViewById(R.id.submit_button)
submitButton.setOnClickListener { /* ... */ }
}
}
lateinit only works with var (not val) and only for non-primitive, non-nullable types. If you access it before initialization, you get an UninitializedPropertyAccessException. Always check ::name.isInitialized if unsure.
lateinit for dependency injection (Spring, Koin), test setup (JUnit @Before), and Android view binding where initialization happens in a lifecycle method. If you can use a nullable type instead, prefer that for safety.
📖 Official docs: Late-Initialized Properties (kotlinlang.org)
Type Aliases
Type aliases give a new name to an existing type. They don't create new types — just more readable names for complex or frequently used type signatures.
Theory
A typealias is resolved at compile time — it does not create a new type, just an alternative name. This means typealias UserId = String and String are fully interchangeable at runtime. Type aliases are especially useful for function types ((T) -> R), generic instantiations with many type parameters, and documenting intent. They can also have visibility modifiers (internal typealias) and can reference generic types with their own parameters (typealias MyMap<K, V> = Map<K, V>).
// Simple aliases for domain concepts
typealias UserId = String
typealias Email = String
// Aliases for complex collection types
typealias Users = List
typealias UserMap = Map
// Usage — much clearer intent
fun fetchUsers(): Users { /* ... */ }
fun findUser(id: UserId): User? { /* ... */ }
// Aliases for function types — great for callbacks
typealias OnClickListener = (View) -> Unit
typealias ApiCallback = (Result) -> Unit
class Button {
fun setOnClickListener(listener: OnClickListener) { /* ... */ }
}
fun fetchData(callback: ApiCallback) { /* ... */ }
fun process(users: Users) is clearer than fun process(users: List<User>). They're especially valuable for nested generics and lambda types in Android callbacks.
Duration (Kotlin 1.6+)
Kotlin provides a type-safe Duration API for working with time intervals. It prevents mixing seconds and milliseconds by accident.
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.milliseconds
val timeout = 30.seconds // Duration
val half = 500.milliseconds // Duration
// Arithmetic and comparison
val total = timeout + half // 30.5 seconds
val isLong = timeout > 1.seconds // true
// Convert
val millis = timeout.inWholeMilliseconds // 30000
// Use with APIs
fun delayFor(duration: Duration) {
println("Delaying for $duration")
}
Long values for timeouts leads to bugs — was that 500 seconds or 500 milliseconds? Duration makes the unit explicit at the type level. The compiler won't let you accidentally pass milliseconds where seconds are expected.
Comments
// Single line comment
/*
* Multi-line comment
* Just like C, Java, JS
*/
/**
* KDoc — Kotlin's documentation comment
* Used to generate API docs
* @param name The user's name
*/
Try It Yourself
Declare a val for your name, a var for your age, and print a sentence using a string template that says:
"My name is [name] and next year I will be [age+1] years old."
// Your code here:
// Run it at play.kotlinlang.org
Stuck? Show hint
val name = "Your Name", var age = 25, and println("My name is $name and next year I will be ${age + 1} years old.")
Concept Check
What will happen if you try to reassign a val?