Understanding mutability and immutability in Scala

It’s been 4 months now since I started working with Scala and although I’m far away from being considered a Scala expert (the language is far more complex to master than Java, C# or any other programming language), I’ve got crystal clear how mutability and immutability works in the language and also generally (because this is actually a general, language agnostic concept). Unfortunately it seems to me that several developers are struggling to get it right, so in this post I’m gonna try my best to make it simple and clear for all.
There are two level of mutability/immutability:

1. Variable level
2. Object level

A variable is mutable if defined using the var keyword, it’s instead immutable if defined with the val keyword.
The immutability of a variable means that once it has been defined it can’t be reassigned to something else, the mutability of a variable instead means that it’s possible to assign another object to the variable after its definition.
For example the following code won’t compile:

object Foo extends App {
    val foo: String = "ciao"
    foo = "hello"
}

It will raise this exception:

error: reassignment to val

This happens because foo is immutable (it’s like a constant, it can’t change).
But if we change val with var all will be fine:

object Foo extends App {
    var foo: String = "ciao"
    foo = "hello"
}

This because foo is now mutable, so we can assign it to another string (one or more times).
BUT regarding object mutability we are not mutating the string object, because a string either referred with val or var is an immutable object! So the reassignment of foo does not change the string object itself, this is not possible, what happens is that a new string object is created (“hello”) and it’s assigned to the variable, later the previous string object “ciao” will be marked for deletion and the garbage collector will get rid off it.
It’s possible to demonstrate that these objects are different by checking their hash code:

object Foo extends App {
    var foo: String = "ciao"
    println(foo.hashCode)
    println(foo.hashCode)
    
    foo = "hello"
    println(foo.hashCode)
    println(foo.hashCode)
    
    foo = "hi"
    println(foo.hashCode)
    println(foo.hashCode)
}

The code above will print 2 codes of the same value for the object “ciao”, 2 codes of the same value for the object “hello” and 2 codes of the same value for the object “hi” (we have created 3 string objects in memory and thus we have 3 different hash codes for each one).
Mutability and immutability are the reason behind different collections implementations.
One of the most common collection is the List object for example and it’s an immutable one.
It’s possible to “sum” several lists into one, but in my opinion is not a good practice, because we are creating several unnecessary temporary objects in memory.
For example:

object Foo extends App {
    var myList: List[Int] = List(1, 2, 3)
    myList = myList ::: List(4, 5, 6)
    myList = myList ::: List(6, 7, 8)
    myList = myList ::: List(9, 10, 11)
}

can be better by using a collection designed for mutability like ListBuffer:

import scala.collection.mutable.ListBuffer

object Foo extends App {
    val buffer: ListBuffer[Int] = ListBuffer(1, 2, 3)
    buffer.append(4, 5, 6)
    buffer.append(6, 7, 8)
    buffer.append(9, 10, 11)
}

and as you can see I defined the buffer as an immutable constant variable, because I do want the buffer to expand (and this is provided by design since it’s a mutable collection) but I don’t want it to be replaced by another buffer with a new assignment… do you get it?