Changing Immutable Objects

Recently someone at work someone sent me a link to an article saying that getters and setters are evil by a guy named Yego256. He has a few other interesting posts about immutable objects here and here. They can be added to the list I made of pages about immutable objects here.

Hopefully Grails 3 will utilize immutability.

It takes a shift in mindset to start using immutable objects. Plus, in Java (and Groovy) a lot of web frameworks don’t utilize them, so people are not too familiar with them.

One issue some developers have is: Many things in the world are mutable. How can you make “changes” to immutable objects?

One answer is that you can use the values you don’t want to change to populate a new immutable object, and your variable/field name points to a new immutable object.

(If you can think of a better way to explain this, let me know. It’s kind of confusing to say that an immutable object is referenced by a “variable name”.)

I will demonstrate this with Groovy (using the annotations EqualsAndHashCode, Immutable and ToString). I made a class to model a state in the USA. States do not change their names, abbreviations or year they joined the union. But many of them have changed their capital cities. So we will make an object, and change its capital city while keeping everything else the same.

Here is our class:

package info.shelfunit.states

import groovy.transform.EqualsAndHashCode
import groovy.transform.Immutable 
import groovy.transform.ToString

@Immutable
@ToString( includeNames = true )
@EqualsAndHashCode
class USState {
    String name
    String abbrev
    String capital
    int yearJoined
}

Now let’s look at using it in a Groovy shell. The first capital of Illinois was a town called Kaskaskia, which no longer exists. Then it moved to Vandalia (which does still exist) and finally to Springfield.

I will try to change a few fields to prove the objects are immutable, and I will call hashCode() to show that we are dealing with different objects.

groovy:000> import info.shelfunit.states.*
===> [import info.shelfunit.states.*]
groovy:000> il = new USState( name: 'Illinois', abbrev: 'IL', capital: 'Kaskaskia', yearJoined: 1818)
===> info.shelfunit.states.USState(name:Illinois, abbrev:IL, capital:Kaskaskia, yearJoined:1818)
groovy:000> il.name
===> Illinois
groovy:000> il.name = "Indiana"
ERROR groovy.lang.ReadOnlyPropertyException:
Cannot set readonly property: name for class: info.shelfunit.states.USState
        at info.shelfunit.states.USState.setProperty (USState.groovy)
        at groovysh_evaluate.run (groovysh_evaluate:3)
        ...
groovy:000> il.toString()
===> info.shelfunit.states.USState(name:Illinois, abbrev:IL, capital:Kaskaskia, yearJoined:1818)
groovy:000> il.hashCode()
===> 1673606286
groovy:000> il = new USState( name: il.name, abbrev: il.abbrev, capital: 'Vandalia', yearJoined: il.yearJoined )
===> info.shelfunit.states.USState(name:Illinois, abbrev:IL, capital:Vandalia, yearJoined:1818)
groovy:000> il.abbrev
===> IL
groovy:000> il.abbrev = "WI"
ERROR groovy.lang.ReadOnlyPropertyException:
Cannot set readonly property: abbrev for class: info.shelfunit.states.USState
        at info.shelfunit.states.USState.setProperty (USState.groovy)
        at groovysh_evaluate.run (groovysh_evaluate:3)
        ...
groovy:000> il.toString()
===> info.shelfunit.states.USState(name:Illinois, abbrev:IL, capital:Vandalia, yearJoined:1818)
groovy:000> il.hashCode()
===> -934597459
groovy:000> il = new USState( name: il.name, abbrev: il.abbrev, capital: 'Springfield', yearJoined: il.yearJoined )
===> info.shelfunit.states.USState(name:Illinois, abbrev:IL, capital:Springfield, yearJoined:1818)
groovy:000> il.capital
===> Springfield
groovy:000> il.capital = "Homerville"
ERROR groovy.lang.ReadOnlyPropertyException:
Cannot set readonly property: capital for class: info.shelfunit.states.USState
        at info.shelfunit.states.USState.setProperty (USState.groovy)
        at groovysh_evaluate.run (groovysh_evaluate:3)
        ...
groovy:000> il.toString()
===> info.shelfunit.states.USState(name:Illinois, abbrev:IL, capital:Springfield, yearJoined:1818)
groovy:000> il.hashCode()
===> 213225124
groovy:000>

So we used the existing values to make new objects. The JVM used the same variable name (“il”) to refer to different immutable objects.

Here is the code without the output:

import info.shelfunit.states.*
il = new USState( name: 'Illinois', abbrev: 'IL', capital: 'Kaskaskia', yearJoined: 1818)
il.name // returns "Illinois"
il.name = "Indiana" // will cause groovy.lang.ReadOnlyPropertyException
il.toString()
il.hashCode()

// Change capital to Vandalia
il = new USState( name: il.name, abbrev: il.abbrev, capital: 'Vandalia', yearJoined: il.yearJoined )
il.abbrev // returns "IL"
il.abbrev = "WI" // causes groovy.lang.ReadOnlyPropertyException:
il.toString()
il.hashCode()

// Change capital to Springfield
il = new USState( name: il.name, abbrev: il.abbrev, capital: 'Springfield', yearJoined: il.yearJoined )
il.capital // returns "Springfield"
il.capital = "Homerville" // causes groovy.lang.ReadOnlyPropertyException:
il.toString()
il.hashCode()

2015-06-06_14.23.56 update: I now realize that in order to prove the objects were truly different objects, I should have called System.identityHashCode(il) instead.

You’re welcome.

Image from the Rheinau Psalter, a 13th century manuscript housed at Central Library of Zurich. Image from e-Codices. This image is assumed to be allowed under Fair Use.