1

Been reading and watching escaping references and I get the idea of why and how to fix it, but I'm still confused on one thing...

What if the data does need to be updated? For example, I copied the code from the blog post down below and also added an additional class member of "email". Say the customer changed his email address. If this structure does not allow the object to be modified, how it the update done then?

Interface:

public interface CustomerReadOnly {
    public String getName();
}

Customer:

public class Customer implements CustomerReadOnly {

    private String name;

    private String email;

    public Customer(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public String toString() {
        return this.name;
    }
}

Records

public class CustomerRecords {

    private Map<String,Customer> customerRecords;

    public CustomerRecords() {
        this.customerRecords = new HashMap<>();
    }

    public CustomerReadOnly getCustomerByName(String name) {
        return this.customerRecords.get(name);
    }
}
  • Not all classes should be completely immutable. Otherwise, you would have to go the String route -- create a new instance every time a field is changed. – Hovercraft Full Of Eels May 13 '20 at 01:50
  • Generally it's good practice to make objects immutable. When you need a change, make a copy. This simplifies understanding the code, and it's more compatible with thread-safeness. Of course if the object is huge, this may not be possible. – Gene May 13 '20 at 01:50

1 Answers1

2

Java solves both issues presented in that article:

  • Shallowly immutable data holders ➙ Records
  • Read-only maps ➙ Map.copyOf

Leaking references is an important issue. But that article’s solution seems overwrought to me in its approach.

  • Certainly creating the CustomerRecords to hold a map seems redundant and useless. Instead, use a non-modifiable map (discussed below).
  • As for a "read-only" interface as view onto a mutable object, this might make sense in some limited situations. But you might also wreak havoc when the supposedly immutable "CustomerReadOnly" returns a different email address on the second call to "getEmail" after an update. Trying to be simultaneously both mutable and immutable is unwise. To handle immutability, instead make an immutable copy of the mutable state.

Records

The Records feature being previewed in Java 14 and previewed again in Java 15, and discussed by Brian Goetz, provide for shallowly immutable data holders. This special kind of class handles automatically the constructor, “getter” accessors, equals, hashCode, and toString methods behind the scenes. So your example class Customer turns into this utterly simple code:

record Customer( String name , String email ) {}

You retain the option of implementing those various methods if need be. For this example here, you might want a constructor to do data validation, ensuring non-null arguments, and specifically a non-empty name, and valid-looking email address.

Read-only Map

Java 10 gained the Map::copyOf method to instantiate a non-modifiable Map using the entries of another Map.

Map< String , Customer > customersByNameReadOnly = Map.copyOf( otherMap ) ;

So no need for defining a CustomerRecords class.

To change immutable data, make a copy

You asked:

What if the data does need to be updated? For example, I copied the code from the blog post down below and also added an additional class member of "email". Say the customer changed his email address.

If your goal is thread-safety through immutable data, then you must deal with copies of your data. If the customer represented by a Customer record object changes their email address, then you must create a new fresh Customer record object, swap any references to the old object, and let the old object go out-of-scope to become a candidate for garbage-collection.

Some folks believe in an everything-should-be-immutable approach, with programming languages designed to cater to such an approach.

Personally, I believe that to be practical some classes should be mutable while other kinds of classes should be immutable. In my view, complex business objects such as an invoice or purchase order should often be mutable. Simpler kinds of data, such as a date, should be immutable. When doing an accounting app, I expect changes to an invoice, but if the date of that invoice suddenly changes because some other invoice happens to be pointing to the same mutable date object via an “escaped reference”, then I would be mightily annoyed. To my mind, the invoice should be mutable while the date object member on that invoice should be immutable.

So I appreciate that Java gives us a “middle way”. Rather than be slavishly devoted to the everything-is-immutable approach, or frustrated by mutability in objects I expect to be stable, with Java we can pick and choose an appropriate route.

  • When immutability is desired, use Java Records for immutable objects, and use Map.of, Set.of, and List.of (or .copyOf) for unmodifiable collections.
  • When mutability is appropriate, use regular Java classes and collections.

As for that “read-only” interface on a mutable object, that seems generally to be confusing at best, and dangerous at worst. I think an object should be clearly and truly immutable or mutable, but not try to be both.

Immutable example: java.time

For examples of this, see the java.time classes that years ago supplanted the terrible date-time classes bundled with the earliest versions of Java. In java.time the classes are immutable by design, and therefore thread-safe.

LocalDate today = LocalDate.now() ;
LocalDate tomorrow = today.plusDays( 1 ) ;

In the code above, we capture the current date as seen in JVM’s current default time zone. Then we add a day to move to "tomorrow". But adding a day does not affect the original object referenced by today variable. Rather than alter (“mutate”) the original, we instantiate a new, second LocalDate object. The new object has values based on values from the original, along with our desired change (adding a day).

The java.time framework even worked out some handy naming conventions. The names to…, from…, parse, format, get…, with, and so on makes dealing with the immutability more intuitive. You may want to follow these conventions in your own coding.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154