Java's final vs. Rust's mut

We create three lists:

    final var list = new ArrayList<Integer>();
    final var asList = Arrays.asList(0, 1, 2);
    final var unmodifiableList = Collections.unmodifiableList(Arrays.asList(0, 1, 2));

Questions:

  • Will final prevent us from calling list.add(0)?
  • Can we, similarly add to either asList or unmodifiableList?
  • What if we attempt to replace the 0th element from either asList or unmodifiableList using .set(0, 6)?

Let's see:

$ java Final 
list as originally assigned: []
list after list.add(0): [0]

asList as originally assigned: [0, 1, 2]
asList after asList.set(0, 6): [6, 1, 2]
caught exception: java.lang.UnsupportedOperationException
asList after asList.add(3): [6, 1, 2]

unmodifiableList as originally assigned: [0, 1, 2]
caught exception: java.lang.UnsupportedOperationException
unmodifiableList after asList.set(0, 6): [0, 1, 2]
caught exception: java.lang.UnsupportedOperationException
unmodifiableList after asList.add(3): [0, 1, 2]

What happened?

  • First, it should be pretty clear final is almost meaningless. Per the language, final simply means you can not reassign the variable, but Java doesn't care what you do with anything that might be in the variable's object
  • Arrays.asList(...) does not allow modications that would change the collection's size, but happily accepts modications that change elements in the collection
  • Collections.unmodifiableList(...) prevents any change, is the only mechanism in this example to produce a truly immutable collection.

So what?

  • UnsupportedOperationException is an unchecked exception. It's not on the signature of any method, and code that wishes to use methods which mutate a collection has no way to enforce that the caller provides a collection supporting mutable operations
  • Behavior is not a matter of a method's enforceable contract (it can't be expressed in the method's declaration), instead what we have is contract-by-documentation (see JavaDoc for Arrays.asList(T ...) and Collections.unmodifiableList(List))
  • The example compiles: as will become a theme here, Java pushes most bugs to runtime.

Our first attempt with Rust is simple enough to show here:

fn main() {
    let vec = vec!(0, 1, 2);
    vec[0] = 6; // reassign the 0th element to 6 from 0
    vec.push(3); // add 3 to the end after two
    println!("{:?}", vec);
}

Let's compile it:

$ rustc immut.rs 
error[E0596]: cannot borrow immutable local variable `vec` as mutable
 --> immut.rs:3:5
  |
2 |     let vec = vec!(0, 1, 2);
  |         --- help: make this binding mutable: `mut vec`
3 |     vec[0] = 6;
  |     ^^^ cannot borrow mutably

error[E0596]: cannot borrow immutable local variable `vec` as mutable
 --> immut.rs:4:5
  |
2 |     let vec = vec!(0, 1, 2);
  |         --- help: make this binding mutable: `mut vec`
3 |     vec[0] = 6;
4 |     vec.push(3);
  |     ^^^ cannot borrow mutably

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0596`.

What happened?

  • Rust variables are immutable by default
  • Mutation is allowed only if you explicitly ask for it via mut
  • Mutation is not a runtime bug, it's caught by the compiler.

This should feel "game changing", but how is this happening?

First, let's look at vec[0] = 6. This we get from the IndexMut<I> trait (we'll discuss traits, Rust's "interface", later):

fn index_mut(&mut self, index: I) -> &mut <Vec<T> as Index<I>>::Output

Python programmers will recognize self, which refers to the vector itself. It's pretty obvious from the signature, though, that to call this function, the vector must be mutable (we'll cover the & next).

Similarly, for vec.push(3), which we get from push:

pub fn push(&mut self, value: T)

That &mut self again! Contrast with len:

pub fn len(&self) -> usize

Here, the vector need not be mutable to call this function.

Rust's mut

  • Is part of a variable's and a function's declaration
  • Is used by the compiler to catch bugs
  • Provides the basis for other guarantees we'll discuss later.

We can fix the previous example by adding a mut declaration to the variable:

fn main() {
    let mut vec = vec!(0, 1, 2);
    vec[0] = 6;
    vec.push(3);
    println!("{:?}", vec);
}

With the following results:

$ rustc mut.rs && ./mut
[6, 1, 2, 3]