Another advantage to immutable objects, which other answers didn't call out explicitly, is that parts of an immutable datastructure can be re-used across several values. For example, consider an array:
a1 = [a, b, c, d, e, f]
Arrays are a very common datastructure. Some languages (e.g. C) let us represent arrays using just a pointer to their start address, others also store their length. Let's do the latter, in pseudocode:
type Array = Pair(Address start, Length length)
If we want to "slice" an array, we just need to adjust the start and length, e.g.
function slice(Array a, Length offset, Length length) {
return Array(start = a.start + offset, length = length);
}
a2 = slice(a1, 3, 2)
Now a2
is the array [d, e]
. Crucially, both a1
and a2
are using the same part of memory: the elements d
and e
aren't copied. This has a few consequences:
- Slicing takes constant time, since we only need to make a pair of ints (we don't need to copy any of the elements).
- Slicing takes constant memory, for the same reason.
- We cannot freeing this memory until all slices have finished using it.
- Mutating the contents of this memory will affect all slices using it.
If we make our arrays mutable then we have to be very careful about point (4): values in one part of the program might be affected by other parts of the program which seem unrelated, if they happen to be sharing the same pieces of memory. Note that this isn't just an issue for concurrent applications; such "aliasing" can undermine a lot of assumptions made by our algorithms, causing them to misbehave. The possibility of aliasing also prevents some optimisations being performed by the compiler.
We can avoid this problem, and problem (3), by having some of our functions make copies of the memory contents, and return pointers to those copies. This is slow and memory-intensive compared to slicing. Many libraries will copy "defensively", i.e. more often than strictly needed by the application, in order to provide a simpler API or to avoid edge-cases.
If we make our arrays immutable then point (4) doesn't matter. Point (3) is still a problem, which can also be solved by copying, but in practice this is much less of a concern: having lots of large arrays sharing the same memory is an advantage; the problem is when we're finished using a large array, but we still need a small slice of it. In this case we must copy that slice in order to free up the large array; however, that only requires copying a small amount of data (by definition), so the cost is usually small.
Immutable data can hence make it easy to write fast, safe, low-memory programs. On the other hand, with mutable data we may be forced to sacrifice one of these (e.g. defensive copying is slow; explicit copying is hard to do safely, or easy to do unsafely). Of course, constructing and garbage-collecting lots of tiny immutable values can also slow things down, so there's a balance to be struck.
Also note that this isn't limited to arrays; there's a whole family of "functional datastructures" which are designed so that common operations can share large parts of the memory without defensive copying; the most common is a singly-linked list, where prepending an element can share the entire "tail".
String
class is an immutable class? Do you really think it was only made immutable because of multithreading in mind? Do you think the String class is useful despite the fact String objects cannot be changed?const
keyword. C has no String class, of course, but the equivalent are arrays of characters, and before C89, there was no way to declare such an array as "immutable". Java (and C#) have no notion of "const" parameters, in those languages it is common to use immutable classes instead.