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
- Create a program that declares both val and var variables, then try to modify each
- Experiment with type inference - declare variables without explicit types
- Write a function that uses lazy initialization for an expensive calculation
- Create examples of nullable and non-nullable variables
- 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
- What's the difference between
val
andvar
? - When should you use explicit type declarations?
- Can you use
lateinit
withval
? - What does
const val
provide thatval
doesn't?
Show answers
val
is immutable/read-only,var
is mutable/changeable- When the type isn't clear from context, for API contracts, or when you need a specific type (like Float vs Double)
- No,
lateinit
can only be used withvar
const val
creates compile-time constants that are inlined, whileval
creates runtime constants