Kotlin - Variables & Constants

Kotlin Variables & Constants

Learn the fundamental difference between mutable variables (var) and immutable values (val) in Kotlin, plus type inference, initialization patterns, and best practices.

The Basics: val vs var

val - Immutable (Read-Only)

Use val for values that won't change after initialization:

val name = "Kotlin"
val pi = 3.14159
val isReady = true

// This would cause a compilation error:
// name = "Java"  // ❌ Cannot reassign val

var - Mutable (Changeable)

Use var for values that need to change:

var counter = 0
var temperature = 20.5
var isComplete = false

// These are allowed:
counter = 10        // ✅ Can reassign var
temperature = 25.0  // ✅ Can change value
isComplete = true   // ✅ Can update state
Best Practice: Prefer val over var when possible. Immutable values make code easier to understand and debug.

Type Inference

Kotlin can automatically determine types based on the assigned value:

Type Inference (Recommended)

val name = "Alice"      // String
val age = 25           // Int
val height = 5.8       // Double
val isStudent = true   // Boolean
val initial = 'A'      // Char

Explicit Types

val name: String = "Alice"
val age: Int = 25
val height: Double = 5.8
val isStudent: Boolean = true
val initial: Char = 'A'

When to Use Explicit Types

// When the type isn't obvious from the value
val data: List<String> = emptyList()
val callback: () -> Unit = { println("Done") }

// When you want a specific numeric type
val smallNumber: Byte = 100
val preciseValue: Float = 3.14f

// For clarity in complex expressions
val result: Double = calculateComplexValue()

Declaration vs Initialization

Declaration with Initialization

val message = "Hello, World!"  // Declared and initialized
var count = 0                  // Declared and initialized

Declaration without Initialization

val name: String        // ❌ Error: must be initialized
var age: Int           // ❌ Error: must be initialized

// Correct ways:
val name: String = "Alice"     // Initialize immediately
var age: Int = 25             // Initialize immediately

Late Initialization

Sometimes you need to declare a variable but initialize it later:

// For var - nullable approach
var userName: String? = null
// Later...
userName = getUserInput()

// For var - lateinit approach (non-null)
lateinit var database: Database
// Later...
database = createDatabase()

// For val - lazy initialization
val expensiveValue: String by lazy {
    performExpensiveCalculation()
}
Teacher Note: lateinit can only be used with var and cannot be used with nullable types or primitive types. lazy is perfect for expensive computations that should only happen when needed.

Scope and Visibility

Local Variables

fun calculateArea() {
    val width = 10.0      // Local to function
    val height = 5.0      // Local to function
    val area = width * height
    
    if (area > 40) {
        val message = "Large area"  // Local to if block
        println(message)
    }
    // message is not accessible here
}

Top-Level Variables

// At the top level of a file
val APP_NAME = "MyApp"           // Available throughout the file
var globalCounter = 0            // Available throughout the file

fun main() {
    println(APP_NAME)            // ✅ Accessible
    globalCounter++              // ✅ Accessible
}

Class Properties

class User {
    val id: Int = 0              // Property (read-only)
    var name: String = ""        // Property (mutable)
    private var password = ""    // Private property
    
    fun updateName(newName: String) {
        name = newName           // ✅ Can modify var property
        // id = 123              // ❌ Cannot modify val property
    }
}

Initialization Patterns

Simple Initialization

val greeting = "Hello"
var counter = 0
val numbers = listOf(1, 2, 3, 4, 5)

Conditional Initialization

val grade = 85
val letterGrade = if (grade >= 90) "A" 
                 else if (grade >= 80) "B"
                 else "C"

val status = when {
    grade >= 90 -> "Excellent"
    grade >= 70 -> "Good"
    else -> "Needs Improvement"
}

Function-Based Initialization

fun getCurrentTime(): String {
    return java.time.LocalDateTime.now().toString()
}

val timestamp = getCurrentTime()
val random = kotlin.random.Random.nextInt(1, 100)

Lazy Initialization

val heavyData: List<String> by lazy {
    println("Computing heavy data...")
    loadDataFromDatabase() // Only called when first accessed
}

fun main() {
    println("Program started")
    // heavyData not computed yet
    
    println(heavyData.size) // Now it's computed
    println(heavyData.size) // Uses cached result
}

Nullable Variables

Nullable vs Non-Nullable

// Non-nullable (default)
val name: String = "Alice"       // Must always have a value
// name = null                   // ❌ Compilation error

// Nullable (with ?)
val optionalName: String? = null // Can be null
val userName: String? = "Bob"    // Or have a value

Working with Nullable Variables

var message: String? = null

// Safe call operator
println(message?.length)  // Prints null, doesn't crash

// Elvis operator (default value)
val length = message?.length ?: 0
println("Length: $length")  // Prints "Length: 0"

// Safe assignment
message = "Hello, Kotlin!"
println(message?.length)  // Prints 14
Beginner Note: Null safety is one of Kotlin's key features. The ? makes it explicit when a variable can be null, preventing many common runtime errors.

Constants and Compile-Time Values

Runtime Constants (val)

val currentTime = System.currentTimeMillis()  // Calculated at runtime
val userName = readUserInput()                 // Determined at runtime

Compile-Time Constants (const val)

const val MAX_USERS = 1000              // Known at compile time
const val APP_VERSION = "1.0.0"        // String literal
const val PI = 3.14159                  // Numeric literal

// const can only be used with:
// - String, primitive types (Int, Double, Boolean, etc.)
// - At top level or in objects
// - With values known at compile time

Object Constants

object AppConstants {
    const val DATABASE_NAME = "myapp.db"
    const val CACHE_SIZE = 50
    const val DEBUG_MODE = true
}

// Usage
fun connectToDatabase() {
    val dbName = AppConstants.DATABASE_NAME
    // ... connect logic
}

Variable Naming Best Practices

✅ Good Names

val userName = "alice"
val isLoggedIn = true
val accountBalance = 1500.0
val MAX_RETRY_COUNT = 3
var currentPageIndex = 0

❌ Poor Names

val x = "alice"        // Too short
val flag = true        // Unclear purpose
val data = 1500.0      // Too generic
val number = 3         // Vague
var i = 0             // Unclear in context

Common Patterns and Idioms

Swapping Variables

var a = 10
var b = 20

// Kotlin's also() function makes swapping elegant
a = b.also { b = a }
println("a = $a, b = $b")  // a = 20, b = 10

Multiple Variable Declarations

// Destructuring declaration
val (name, age) = Pair("Alice", 30)
val (first, second, third) = Triple("A", "B", "C")

// Multiple variables from function
fun getCoordinates() = Pair(10, 20)
val (x, y) = getCoordinates()

Default Values Pattern

fun greet(name: String? = null) {
    val displayName = name ?: "Guest"
    println("Hello, $displayName!")
}

greet()          // Hello, Guest!
greet("Alice")   // Hello, Alice!

Performance Considerations

val vs const val

const val COMPILE_TIME = "Fast"     // Inlined at compile time
val RUNTIME = "Calculated"          // Stored as field

// For frequently accessed values, prefer const val when possible

Lazy vs Immediate Initialization

// Immediate - computed right away
val immediateData = loadLargeDataset()

// Lazy - computed only when needed
val lazyData by lazy { loadLargeDataset() }

// Use lazy for expensive operations that might not be needed
Architecture Note: In large applications, prefer immutable data structures and val declarations. This leads to more predictable code and easier concurrent programming.

Common Mistakes and Solutions

Mistake 1: Using var When val Would Work

// ❌ Unnecessary mutability
var name = "Alice"
println("Hello, $name")

// ✅ Better - use val
val name = "Alice"
println("Hello, $name")

Mistake 2: Not Using Type Inference

// ❌ Unnecessary verbosity  
val name: String = "Alice"
val count: Int = 0

// ✅ Let Kotlin infer types
val name = "Alice"
val count = 0

Mistake 3: Incorrect lateinit Usage

// ❌ Wrong - primitives can't be lateinit
lateinit var count: Int

// ✅ Correct - use nullable or initialize
var count: Int? = null
// or
var count: Int = 0

Practice Exercises

  1. Create a program that declares both val and var variables, then try to modify each
  2. Experiment with type inference - declare variables without explicit types
  3. Write a function that uses lazy initialization for an expensive calculation
  4. Create examples of nullable and non-nullable variables
  5. Practice const val declarations for application constants

Real-World Examples

// User profile example
class UserProfile {
    val userId: String = generateUniqueId()        // Immutable once set
    var displayName: String = "Anonymous"          // Can be updated
    var lastLoginTime: Long? = null                // Optional, starts null
    
    companion object {
        const val MAX_NAME_LENGTH = 50             // Application constant
        const val DEFAULT_AVATAR = "default.png"  // Won't change
    }
}

// Configuration example
object AppConfig {
    const val API_BASE_URL = "https://api.example.com"
    const val REQUEST_TIMEOUT = 30_000  // 30 seconds
    const val MAX_RETRIES = 3
    
    val buildTime: String by lazy {
        java.time.Instant.now().toString()
    }
}

Quick Quiz

  1. What's the difference between val and var?
  2. When should you use explicit type declarations?
  3. Can you use lateinit with val?
  4. What does const val provide that val doesn't?
Show answers
  1. val is immutable/read-only, var is mutable/changeable
  2. When the type isn't clear from context, for API contracts, or when you need a specific type (like Float vs Double)
  3. No, lateinit can only be used with var
  4. const val creates compile-time constants that are inlined, while val creates runtime constants