Mutable Objects

Introduction

Many classes that we build contain mutator methods (methods that allow us to change the content of an object). Objects that can be changed via mutators are said to be mutable. For example:

public class Employee
{
    String name;
    int employee_id;
    public Employee(String name, int employee_id)
    {
        setName(name);
        setId(employee_id);
    }
    
    public void setName(String name)
    {
        if (name == null)
        {
            throw new NullPointerException("Employee's name must not be null");
        }
        
        this.name = name;
        
    }
    
    public String getName()
    {
        return name;
    }
    
    public void setId(int employee_id)
    {
        this.employee_id = employee_id;
    }
    
    public int getId()
    {
        return employee_id;
    }
    
    public boolean equals(Employee e)
    {
        return employee_id == e.employee_id;
    }
    
    public int compareTo(Employee e)
    {
        return employee_id - e.employee_id;
    }
}

There are other examples, such as Java's Random class (because of all of its next methods), StringBuilder class (because of the append method, among others), and ArrayList class (because of add and remove).

Some classes do not contain mutator methods. Objects from these classes are said to be immutable. For example, Java's String class cannot alter the string once the object is created. The wrapper classes (e.g. Integer class and Double class) allow accessors but no mutators.

Implications for Data Structures

Being immutable can make things difficult and inefficient. Actions that could be simple as a mutation require more work if a new object must be created. For example, to concatenate strings together, we must create and assign a new object rather than just append the string to the existing object. For example, this makes use of three String objects:

String S1 = "Hello ";
S1 = S1 + "there";

If done repeatedly, this can cause a lot of overhead.

However, immutable objects offer a huge advantage over mutable objects. Consider the data structures we've looked at this semester. When we add an object to one of them, it doesn't mean we give up outside access to the object. If we subsequently alter the object "external" to the collection, we could destroy a property of the collection. See an example on the board.

For many of the data structures seen this semester, this isn't a major problem (and could even be an advantage). There are some data structures where this could be a problem. Which data structures are they?

What can we do to address this problem? We could make the objects immutable, but that only works if we are the ones creating the objects being stored in the data structure. However, there will be times when we're in charge of making the collection data structure and have no control over what is stored in it (other than polymorphic requirements). Another option is to put clones of the original objects into the collection. We still must be careful not to mutate the objects within the collection since some access methods return references to the objects within the collection. To be very safe our accessors should themselves return clones of the objects rather than references to the originals, but that could also create problems (why?).

Cloning

Java objects can be copied using the clone() method. It is defined in class Object, so it will work for all Java classes. However, you must override it for new classes to work properly (and it is also a nice idea to declare that the class implements the Cloneable interface). It needs to know what data in the new class to copy. This is somewhat tricky to do, especially for subclasses (see Employee.java for syntax). Some classes already define the clone() method, such as Java primitive arrays, so we can use it without overriding it.

The clone() method is typically defined to do a shallow copy of the data in an object. This means that when the object is copied, objects that it refers to are not copied. For example, if cloning an array of StringBuilders, we get a new array but not new StringBuilders. This can cause data sharing/aliasing that you must be aware of. For an example, see ShallowCopyExample.java and Employee.java.

Generally speaking, (true) deep copying is more difficult than shallow copying. For deep copying, we need to follow all references in the original and make copies for the clone(). This could be several levels deep. A good example of this is a Binary Tree. A Binary Tree has only one instance variable: a reference to the root node. A shallow copy would only copy this single reference. A deep copy would have to traverse the entire tree, copying each node and copying the data in each node and ...

<< Previous Notes Daily Schedule