var vs val in Kotlin
One of the first and most important decisions you make in Kotlin is whether a value can change or must remain constant. Kotlin makes this decision explicit using two keywords: var for mutable values and val for immutable ones. Understanding this distinction will make your code safer, cleaner, and easier to reason about.
What is an Identifier?
An identifier is simply a name that refers to data in your program. Before declaring any identifier, you must choose whether it should be mutable or immutable.
val count = 10 // Immutable
val userName = "Alice" // Immutable
var score = 0 // Mutable
Identifier Naming Rules
- Must start with a letter or underscore (
_) - Can contain letters, numbers, and underscores
- Case-sensitive (
total≠Total) - Follow camelCase convention for variables
The Core Question
Before choosing var or val, always ask yourself:
Should this value ever change after it is created?
Your answer determines which keyword you use.
var — Mutable (Changeable)
Use var when the value must change during execution.
Syntax
var identifier = initialValue
Simple Example
fun main() {
var score = 10
score = 15
score += 5
println(score) // Output: 20
}
Key points:
scorechanges multiple times- This is valid because it's declared with
var - The compiler allows reassignment
When var Makes Sense
Some values naturally change during program execution:
| Use Case | Example |
|---|---|
| Counters | var attempts = 0 |
| Running totals | var sum = 0 |
| Loop variables | var index = 0 |
| Temporary state | var isProcessing = false |
In such cases, mutation is acceptable and sometimes necessary.
val — Immutable (Preferred)
A val can be assigned only once. After initialization, it cannot be changed.
Syntax
val identifier = initialValue
Example
fun main() {
val maxRetries = 3
// maxRetries = 5 // ❌ Compile-time error: Val cannot be reassigned
println("Max retries: $maxRetries")
}
Benefits:
- Kotlin prevents reassignment at compile time
- Errors are caught early, before the program runs
- Intent is clear to other developers
Side-by-Side Comparison
- Using val (Recommended)
- Using var
fun main() {
val coffeeTime = 11
println("Coffee break at: $coffeeTime")
}
Advantages:
- ✅ Clear intent
- ✅ Cannot change accidentally
- ✅ Safer design
- ✅ Better for concurrency
fun main() {
var coffeeTime = 11
coffeeTime = 12 // Value can change
println("Coffee break at: $coffeeTime")
}
Considerations:
- ⚠️ Allows change
- ⚠️ Requires more caution
- ⚠️ Can lead to bugs if changed unexpectedly
Why Kotlin Encourages val
Kotlin encourages immutability by default. Code is easier to understand when values don't change unexpectedly. Immutable data structures are inherently thread-safe and lead to fewer bugs.
Benefits of using val
- Easier to read and reason about — You know the value won't change
- Fewer bugs — No accidental modifications
- Better concurrency support — Immutable data is thread-safe
- Cleaner APIs — Functions with immutable parameters are predictable
Practical Examples
Example 1: Configuration Values
class AppConfig {
val apiUrl = "https://api.example.com" // Never changes
val timeout = 30_000L // Never changes
var retryCount = 0 // Changes with each retry
}
Example 2: Calculating Totals
fun calculateTotal(prices: List<Int>): Int {
var total = 0 // Mutable accumulator
for (price in prices) {
total += price
}
return total
}
fun main() {
val prices = listOf(10, 20, 30) // Immutable list reference
val result = calculateTotal(prices)
println("Total: $result") // Output: Total: 60
}
Analysis:
totalisvarbecause it accumulates valuespricesisvalbecause the list reference doesn't change- Mutation is local, controlled, and easy to understand
Example 3: Good vs Bad Naming
- Bad (Unclear Intent)
- Good (Self-Explanatory)
val x = 11
val data = "John"
val temp = true
Problems:
- Unclear purpose
- Requires context to understand
- Poor maintainability
val coffeeBreakHour = 11
val userName = "John"
val isAuthenticated = true
Benefits:
- Clear intent
- Self-documenting
- Follows camelCase convention
Common Pitfalls
Don't do this:
var userName = "Alice" // Never changes after initialization
var maxConnections = 100 // Never changes after initialization
Do this instead:
val userName = "Alice"
val maxConnections = 100
Start with val by default. Only switch to var when you encounter a compilation error or have a clear need for mutability.
// ❌ WRONG: var not needed
fun getUserGreeting(name: String): String {
var greeting = "Hello, $name!"
return greeting
}
// ✅ CORRECT: val is sufficient
fun getUserGreeting(name: String): String {
val greeting = "Hello, $name!"
return greeting
}
// ✅ EVEN BETTER: Direct return
fun getUserGreeting(name: String): String {
return "Hello, $name!"
}
Avoid generic names that don't convey meaning:
| ❌ Avoid | ✅ Prefer |
|---|---|
x, y, z | width, height, depth |
temp, tmp | processedUser, filteredList |
data | userData, configData |
flag | isActive, hasPermission |
// ❌ WRONG: Mutable state accessible everywhere
class UserService {
var currentUser: User? = null // Dangerous global state
fun login(user: User) {
currentUser = user
}
}
// ✅ CORRECT: Encapsulate mutable state
class UserService {
private var _currentUser: User? = null
val currentUser: User?
get() = _currentUser
fun login(user: User) {
_currentUser = user
}
}
Best Practices
Always start with val. Only use var when the compiler forces you to or when you have a clear, justified reason for mutability.
fun processOrders(orders: List<Order>) {
val filteredOrders = orders.filter { it.isValid } // val by default
val totalAmount = filteredOrders.sumOf { it.amount }
println("Processed ${filteredOrders.size} orders")
println("Total amount: $totalAmount")
}
When you must use var, keep it in the smallest scope possible:
fun findMaxValue(numbers: List<Int>): Int {
var max = numbers.firstOrNull() ?: 0 // Local scope only
for (number in numbers) {
if (number > max) {
max = number
}
}
return max
}
Replace mutable accumulators with functional operations when possible:
// Instead of this:
fun sumPrices(items: List<Item>): Int {
var total = 0
for (item in items) {
total += item.price
}
return total
}
// Prefer this:
fun sumPrices(items: List<Item>): Int {
return items.sumOf { it.price }
}
Use read-only collection types by default:
// ✅ GOOD: Read-only list
val numbers: List<Int> = listOf(1, 2, 3)
// ⚠️ CAUTION: Mutable list (use only when necessary)
val mutableNumbers: MutableList<Int> = mutableListOf(1, 2, 3)
mutableNumbers.add(4)
Key Takeaways
valcreates immutable references that cannot be reassigned after initializationvarcreates mutable references that can be changed during execution- Always prefer
valby default for safer, more predictable code - Use
varonly when you have a clear need for mutability (counters, accumulators, etc.) - Immutability leads to fewer bugs, better concurrency, and more maintainable code
- Good variable naming makes the difference between clear and confusing code
Related Resources
External References: