Beginner

Variables and Types in Kotlin

Learn about variables, data types, null safety, and type conversions in Kotlin with practical Docker-ready examples

Kotlin’s type system is one of its defining features. It combines the safety of static typing with the convenience of type inference, so you rarely need to write explicit type annotations while still getting full compile-time checking. Most notably, Kotlin distinguishes between nullable and non-nullable types at the language level, eliminating the null pointer exceptions that plague Java code.

As a multi-paradigm language blending object-oriented and functional styles, Kotlin encourages immutability by default. The val keyword declares read-only bindings while var declares mutable ones — a design choice that nudges developers toward safer, more predictable code. In this tutorial, you’ll explore Kotlin’s type system, variable declarations, type conversions, and the null safety features that set it apart.

Variable Declarations: val vs var

Kotlin provides two keywords for declaring variables. The val keyword creates a read-only reference (similar to final in Java), while var creates a mutable reference that can be reassigned. Kotlin’s type inference means the compiler can determine the type from the assigned value, though you can always declare types explicitly.

Create a file named Variables.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
fun main() {
    // Immutable (read-only) variables with val
    val language: String = "Kotlin"        // Explicit type
    val version = 2.0                      // Inferred as Double
    val year = 2011                        // Inferred as Int
    val isModern = true                    // Inferred as Boolean

    println("=== val (read-only) variables ===")
    println("Language: $language")
    println("Version: $version")
    println("Year: $year")
    println("Is modern: $isModern")

    // Mutable variables with var
    var counter = 0
    println("\n=== var (mutable) variables ===")
    println("Counter before: $counter")
    counter = 10
    println("Counter after: $counter")

    // Basic types
    println("\n=== Basic Types ===")
    val byte: Byte = 127                   // 8-bit signed integer
    val short: Short = 32767               // 16-bit signed integer
    val int: Int = 2_147_483_647           // 32-bit signed integer (underscores for readability)
    val long: Long = 9_000_000_000L        // 64-bit signed integer
    val float: Float = 3.14F               // 32-bit floating point
    val double: Double = 3.141592653589793 // 64-bit floating point
    val char: Char = 'K'                   // Single character
    val bool: Boolean = true               // Boolean

    println("Byte: $byte")
    println("Short: $short")
    println("Int: $int")
    println("Long: $long")
    println("Float: $float")
    println("Double: $double")
    println("Char: $char")
    println("Boolean: $bool")

    // Strings
    println("\n=== Strings ===")
    val greeting = "Hello, Kotlin"
    val multiline = """
        |This is a
        |multiline string
        |with trimmed margins
    """.trimMargin()

    println("Greeting: $greeting")
    println("Length: ${greeting.length}")
    println("Multiline:\n$multiline")

    // Type conversions (explicit — no implicit widening in Kotlin)
    println("\n=== Type Conversions ===")
    val intVal = 42
    val longVal = intVal.toLong()
    val doubleVal = intVal.toDouble()
    val stringVal = intVal.toString()
    val backToInt = "123".toInt()

    println("Int: $intVal")
    println("To Long: $longVal")
    println("To Double: $doubleVal")
    println("To String: \"$stringVal\" (type: String)")
    println("String to Int: $backToInt")
}

Null Safety

Kotlin’s null safety system is built into the type system itself. By default, variables cannot hold null values. To allow nulls, you must explicitly declare the type as nullable using the ? suffix. This forces you to handle the null case, preventing null pointer exceptions at compile time rather than runtime.

Create a file named NullSafety.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
fun main() {
    // Non-nullable types (default)
    val name: String = "Kotlin"
    // val invalid: String = null  // Won't compile!

    // Nullable types use the ? suffix
    var nickname: String? = "K"
    println("=== Null Safety ===")
    println("Name: $name")
    println("Nickname: $nickname")

    nickname = null
    println("Nickname after null: $nickname")

    // Safe call operator (?.) — returns null if the receiver is null
    println("\n=== Safe Calls ===")
    println("Nickname length: ${nickname?.length}")        // prints: null
    println("Name length: ${name.length}")                 // prints: 6

    // Elvis operator (?:) — provides a default for null
    val displayName = nickname ?: "No nickname"
    println("Display: $displayName")

    // Safe calls with chaining
    val input: String? = "hello, world"
    val result = input?.uppercase()?.take(5)
    println("Chained safe call: $result")

    // Not-null assertion (!!) — throws if null, use sparingly
    val definitelyNotNull: String? = "I exist"
    println("Asserted length: ${definitelyNotNull!!.length}")

    // Smart casts with null checks
    println("\n=== Smart Casts ===")
    val maybeNull: String? = "Kotlin"
    if (maybeNull != null) {
        // Compiler knows maybeNull is non-null here
        println("Smart cast length: ${maybeNull.length}")
    }

    // let — execute block only when non-null
    nickname?.let {
        println("This won't print because nickname is null")
    }

    val restored: String? = "Back again"
    restored?.let {
        println("Let block: $it has ${it.length} chars")
    }
}

Constants and Compile-Time Values

Kotlin distinguishes between runtime read-only values (val) and true compile-time constants (const val). The const modifier can only be applied to top-level or object properties with primitive or String types.

Create a file named Constants.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Compile-time constants (must be top-level or in object/companion)
const val MAX_USERS = 1000
const val APP_NAME = "CodeArchaeology"
const val PI = 3.14159265358979

// Object for grouping constants (similar to Java's static final)
object Config {
    const val VERSION = "1.0"
    const val DEBUG = false
}

fun main() {
    println("=== Constants ===")
    println("Max users: $MAX_USERS")
    println("App name: $APP_NAME")
    println("Pi: $PI")
    println("Version: ${Config.VERSION}")
    println("Debug: ${Config.DEBUG}")

    // val is read-only but computed at runtime
    val currentTime = System.currentTimeMillis()
    println("\n=== Runtime val vs const val ===")
    println("Current time (runtime val): $currentTime")
    println("App name (const val): $APP_NAME")

    // Type checking with 'is' and smart casts
    println("\n=== Type Checking ===")
    val items: List<Any> = listOf(42, "hello", 3.14, true, 'K')

    for (item in items) {
        val description = when (item) {
            is Int -> "Int: $item (doubled: ${item * 2})"
            is String -> "String: \"$item\" (length: ${item.length})"
            is Double -> "Double: $item"
            is Boolean -> "Boolean: $item"
            is Char -> "Char: '$item'"
            else -> "Unknown: $item"
        }
        println(description)
    }
}

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the Kotlin image
docker pull zenika/kotlin:1.4

# Run the variables example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 sh -c "kotlinc Variables.kt -include-runtime -d variables.jar && java -jar variables.jar"

# Run the null safety example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 sh -c "kotlinc NullSafety.kt -include-runtime -d nullsafety.jar && java -jar nullsafety.jar"

# Run the constants example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 sh -c "kotlinc Constants.kt -include-runtime -d constants.jar && java -jar constants.jar"

Expected Output

Variables.kt:

=== val (read-only) variables ===
Language: Kotlin
Version: 2.0
Year: 2011
Is modern: true

=== var (mutable) variables ===
Counter before: 0
Counter after: 10

=== Basic Types ===
Byte: 127
Short: 32767
Int: 2147483647
Long: 9000000000
Float: 3.14
Double: 3.141592653589793
Char: K
Boolean: true

=== Strings ===
Greeting: Hello, Kotlin
Length: 13
Multiline:
This is a
multiline string
with trimmed margins

=== Type Conversions ===
Int: 42
To Long: 42
To Double: 42.0
To String: "42" (type: String)
String to Int: 123

NullSafety.kt:

=== Null Safety ===
Name: Kotlin
Nickname: K
Nickname after null: null

=== Safe Calls ===
Nickname length: null
Name length: 6
Display: No nickname
Chained safe call: HELLO
Asserted length: 7

=== Smart Casts ===
Smart cast length: 6
Let block: Back again has 10 chars

Constants.kt (currentTime will vary):

=== Constants ===
Max users: 1000
App name: CodeArchaeology
Pi: 3.14159265358979
Version: 1.0
Debug: false

=== Runtime val vs const val ===
Current time (runtime val): 1712150400000
App name (const val): CodeArchaeology

=== Type Checking ===
Int: 42 (doubled: 84)
String: "hello" (length: 5)
Double: 3.14
Boolean: true
Char: 'K'

Key Concepts

  • val vs var: Prefer val (read-only) by default; use var only when mutation is necessary. This encourages immutable, predictable code.
  • Type inference: Kotlin infers types from assigned values, reducing verbosity while maintaining full static type safety.
  • No implicit conversions: Unlike Java, Kotlin requires explicit conversion between numeric types (e.g., intVal.toLong()), preventing subtle precision bugs.
  • Null safety: The ? suffix marks nullable types. The compiler enforces null checks, eliminating null pointer exceptions at compile time.
  • Safe call operator (?.): Chains operations on nullable values without manual null checks, returning null if any link in the chain is null.
  • Elvis operator (?:): Provides concise default values for nullable expressions, replacing verbose if-null-then patterns.
  • Smart casts: After a null check or type check, the compiler automatically casts the variable to the appropriate type within that scope.
  • const val: True compile-time constants for primitives and strings, enforced at the language level — distinct from runtime val bindings.

Running Today

All examples can be run using Docker:

docker pull zenika/kotlin:1.4
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining