One of the hardest concepts to get right when programming is managing state and controlling how data changes. Which language we use can be crucial for writing clean, maintainable, and bug-free code. Kotlin, a modern programming language, includes in the toolbelt of it’s fundamental concepts a powerful feature known as explicit mutability. This feature, implemented through the use of val and var keywords, provides developers with fine-grained control over data mutability. In this blog post we’ll explore the ins and outs of explicit mutability in Kotlin, its benefits, best practices, and highlight briefly how it compares to other languages.

Understanding Mutability

Before we dive into Kotlin’s approach, let’s briefly discuss what mutability means in programming. In simple terms mutability refers to the ability to change or modify data after it’s been created. Mutable data can be altered, while immutable data remains constant throughout its lifetime.

Kotlin’s Approach: val and var

Kotlin has two keywords for variable declaration: val and var. These keywords explicitly define whether a variable or field is immutable (val) or mutable (var).

  • val (Value) - Immutable References: When you declare a variable with ‘val’, you’re creating an immutable reference. This means that once a value is assigned to the variable, it cannot be reassigned. It’s similar to using the final keyword in Java. For example:
    1
    2
    
     val name = "Alice"
     // name = "Bob"  // Compilation error
    
  • var (Variable) - Mutable References: Variables and fields declared with ‘var’ are mutable, meaning their value can be changed after initial assignment. Example:
    1
    2
    
     var age = 25
     age = 26  // This is perfectly valid
    

Why Explicit Mutability?

Code Clarity

By using val and var, you’re making your intentions clear to your future self and other developers. Anyone reading the code can immediately understand whether a variable is meant to be changed or not.

Preventing Accidental Modifications

Using ‘val’ for variables that shouldn’t change helps prevent accidental modifications. The compiler will catch any attempts to reassign a ‘val’ variable, reducing the risk of unintended side effects.

Thread Safety

Immutable data is inherently thread-safe. When you’re working with concurrent code, using immutable data wherever possible can help avoid race conditions and other concurrency issues.

Functional Programming Support

Kotlin supports some of the functional programming concepts, and the use of immutable data aligns well with functional programming principles. It encourages writing pure functions and promotes easier reasoning about code behavior and state mutations.

Immutability vs. Read-Only

It’s important to understand that ‘val’ creates a read-only reference, which is not always the same as true immutability. Let’s explore this concept further:

Immutable References to Mutable Objects

When you use ‘val’ with a mutable object, the reference is immutable, but the object’s internal state can still be modified.

1
2
3
val list = mutableListOf(1, 2, 3)
list.add(4)  // This is valid, as we're modifying the list's content, not the reference
// list = mutableListOf(5, 6)  // This would be a compilation error

In the example above the list reference is read only byt list itself is mutable.

Smart Casts and Immutability

Kotlin’s smart cast feature works with ‘val’ declarations but not with ‘var’.

1
2
3
4
5
6
var mutableVar: Any = ""
fun failingSmartCastOfVar() {
    if (mutableVar is String) {
        print(mutableVar.length) // Compilation failure
    }
}

The example above will fail to compile because the compiler can guarantee that a ‘val’ reference won’t change between the type check and its usage.

Properties in Classes

When declaring properties in a class, ‘val’ creates a read-only property with a generated getter, while ‘var’ creates a mutable property with both a getter and a setter.

1
2
3
4
class Person(
    val name: String,  // Read-only property (has a getter)
    var age: Int      // Mutable property (has both getter and setter)
) 

Best Practices And Consideration When Using val/var

Prefer ‘val’ By Default

Start by declaring all variables with ‘val’. Only change to ‘var’ if you need to reassign the variable later. This approach, known as “immutability by default,” helps create more predictable and maintainable code.

Use ‘var’ Judiciously

When you do need mutable state, use ‘var’. But try to limit the scope of mutable variables as much as possible. Consider if you can make the variable local to a function instead of a class property.

Immutable Collections

Kotlin provides both mutable and immutable versions of collections.

1
2
val immutableList = listOf(1, 2, 3)
val mutableList = mutableListOf(1, 2, 3)

Prefer using immutable collections (List, Set, Map) over their mutable counterparts (MutableList, MutableSet, MutableMap) when the collection doesn’t need to be modified. Use mutable collections in limited scope and not return them, e.g. only in the body of the function but convert to immutable before returning.

Additionally Kotlin offers a concise and efficient way to create immutable collections using builder functions buildXXX. Those higher-order functions allow you to construct collections using a mutable builders within its lambda, providing a clean syntax for adding elements conditionally or in loops. The resulting collection is immutable, e.g:

1
2
3
4
5
buildList {
    for (i in 0..10) {
        add(i)
    }
}

Collection builder functions are particularly useful when you need to create complex collections based on certain logic or transformations, without the verbosity of creating a mutable collections and converting it afterward.

Data Classes and Immutability

When creating data classes, consider making properties ‘val’ by default. If you need mutable properties, you can always use ‘var’, but immutable data classes are easier to work with and reason about.

1
2
3
4
5
data class User(
    val id: Int, 
    val name: String, 
    var lastLoginDate: Date
)

Const Val for Compile-Time Constants

For top-level or object declarations that are known at compile-time, use ‘const val’. This creates a true compile-time constant, which can be more efficient.

1
2
3
object Constants {
    const val MAX_COUNT = 100
}

Backing Properties

Sometimes you might want to expose a read-only property while keeping a mutable backing field. Kotlin allows this through backing properties:

1
2
3
4
5
6
7
8
9
class Counter {
    private var _count = 0
    
    val count get() = _count
    
    fun increment() {
        _count++
    }
}

Delegated Properties

Kotlin’s property delegation allows you to reuse common property patterns. You can create a read-only property that is calculated on-demand using ‘by lazy’:

1
2
3
4
5
val expensiveComputation: Int by lazy {
    println("Computing...")
    // Complex computation here
    42
}

Late-Initialized Properties

Sometimes you need to have a non-null var property that’s initialized after the constructor. Kotlin provides the lateinit modifier for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyTest {
    lateinit var subject: String

    @BeforeTest
    fun setup() {
        subject = "Some value"
    }

    @Test 
    fun testLength() {
        assertEquals(subject.length, 10)
    }
}

Inline Value Classes and Immutability

Kotlin’s inline value classes are a great way to create type-safe wrappers with zero runtime overhead. These type of class can be used to wrap domain values and inherently immutable:

1
2
3
4
5
6
7
8
9
@JvmInline
value class Password(
    val value: String
)

@Test
fun `example inline class test`() {
    assertEquals(Password("secret").value, "secret")
}

Challenges and Considerations

While Kotlin’s explicit mutability is a great feature, it’s not come without some challenges:

Learning Curve

Developers coming from languages without explicit mutability might need time to adjust to thinking about mutability for every variable they declare.

Overuse of var

Using var excessively can lead to code that is difficult to understand and maintain. To avoid this, always consider if a variable truly needs to be mutable and try to refactor your code to use val whenever possible.

Forgetting to Use Immutable Collections

It’s easy to default to mutable collections out of habit. However, mutable collections can introduce unexpected changes and bugs. Always evaluate if an immutable collection can be used instead.

Performance Considerations

While immutability can lead to safer code, it can sometimes come with a performance cost, especially when working with large data structures that need frequent updates.

Conclusion

Kotlin’s Explicit Mutability, implemented through ‘val’ and ‘var’, is a fundamental feature that promotes safer, more predictable code. By making immutability the default and requiring explicit declaration for mutable variables, Kotlin encourages developers to think carefully about state management in their applications.

As with any language feature, the key to leveraging Explicit Mutability effectively lies in understanding its nuances and applying it judiciously. By following best practices and considering the principles of immutability, developers can write Kotlin code that is not only functional but also maintainable and robust.

Whether you’re building Android apps, server-side applications, or multiplatform projects, mastering Kotlin’s approach to mutability will serve you well in creating high-quality software. So next time you’re declaring a variable in Kotlin, take a moment to consider: should it be a ‘val’ or a ‘var’? Your future self (and your team) will thank you for it.