Why is Java making so many things immutable?

October 8, 2021 | 9 minute read
Text Size 100%:

As Java takes on more characteristics of a functional programming language, it’s carefully moving away from mutable objects.

Download a PDF of this article

Why do new Java features emphasize immutable object types?

For example, early in Java’s history developers saw the JavaBeans specification, which emphasized creating and using mutable objects via set and get methods—but String objects have always been immutable. Then Java 8 replaced the old java.util.Date API, which created mutable objects, with the new immutable java.time API, where all objects are immutable. Then Java 14 previewed and Java 16 added Records, with all fields immutable. Is there a trend here? What’s up with that?

To answer the question, this article looks at strings, dates, and records.

(If you are looking for a tl;dr answer, immutability offers thread safety, while continuous improvements to JVM performance often make it faster to create new objects instead of changing old objects.)

Immutable strings and things

In the beginning, there was Java 1.0, and within it there was the String class. The String class is final and has no mutator methods. Once a string has been constructed, it never changes. Methods that in some languages might change the string, such as toUpperCase(), in Java create a new String with the desired changes applied. A String is immutable, period, full stop (unless you bring in some native code using JNI to modify one).

The need for immutable String objects was clearly understood by the team who created Java: If String objects could be modified, Java’s entire security model would be broken. Here’s a sketch of one potential attack.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
Good Thread: Open File xyz.
  InputStream Constructor; call security manager.
    Security manager - Read file xyz - Permission is OK.
Bad Thread wakes up at just this moment.
  Changes file name string from 'xyz' to '/etc/passwd'
  Yields the CPU
Good Thread
  InputStream Constructor: pass /etc/passwd to operating system open syscall
Bad Thread examines memory buffer for useful information to steal

Furthermore, the immutability of String objects can lead to better multithreaded performance, as these objects do not require synchronization or checking when used in multithreaded code. The application, and the JVM, can take for granted that a String object’s value will never change.

The early Java team didn’t apply the same notion of immutability to normal data objects, such as Dates. If they had, Java might have been an unpopular functional programming (FP) language instead of a popular object-oriented programming (OOP) language, and I wouldn’t have written this.

A bit about functional programming. FP is a software development paradigm, as is OOP. There is no agreed-upon description of FP, but FP is generally taken to include the following:

  • Code as data: Code can be treated as data, and it can be created, passed into, and returned from functions.
  • Pure functions have no side effects—that is, there is no global data.
  • Data is immutable.

Java can never become a pure FP language; there’s simply too much existing Java code using setters and getters. However, Java can never become a pure OOP language either—Java’s eight primitive types ensure that. (Compare with Python, in which even the lowly integer is an object type.)

Java has, however, adopted some of FP’s practices, or at least it is moving in that direction. String objects are immutable, as discussed above, and record objects are too, as discussed below. Java’s lambdas and method references come pretty close to the code-as-data concept. You can learn more from the numerous books on functional programming for Java developers.

JavaBeans and their setters and getters

Java burst onto the scene in 1995, and shortly thereafter developers saw the arrival of the JavaBeans Specification. What’s that? To quote from that original spec, “A Java Bean is a reusable software component that can be manipulated visually in a builder tool.” (At the time, the visual desktop application and browser markets were in Java’s sights; the server-side enterprise market was still moving into range.)

To put it in more modern terms, a JavaBean is a reasonably plain Java object with the following properties:

  • It must provide set and get methods for each property, usually backed by a private field.
  • It must expect to be serialized at design time or runtime or both, and thus implement java.io.Serializable.
  • It should use a standard event model, subclassing java.util.EventObject when its data changes.
  • It should provide support for design-time customization, such as in a GUI builder application if it is a visual bean.

The JavaBeans model and specification have been enormously influential on the Java ecosystem, guiding the development of such competing APIs as Spring Beans and Enterprise JavaBeans. They have also led generations of Java programmers to believe that the natural state of affairs is for objects to have mutable state and set and get methods for each property. Well, that is normal for OOP, but it does not lend itself well to a functional style of programming.

  • Functional programming uses immutable objects.
  • JavaBeans are mutable objects.

Which is better? Both the OOP and FP paradigms have their place, but it seems clear that the trend is toward FP-style development with immutable objects. One example of this in Java is the current Date/Time API. Another is the record structure.

The Date/Time API

The date/time library, java.time, introduced with Java 8, is based almost entirely on Stephen Colebourne’s Joda Time package (the J is pronounced like the J in Java). Joda Time is one of several alternatives to Java 1.0’s original java.util.Date classes.

Did the world really need another date/time package? The list of things wrong with Java 1.0’s date/time handling could fill an entire web page. In fact, it has.

Ignore the weirdness such as having to add and subtract 1,900 to work with the current year. Ignore that a Java 1.0 Date doesn’t really represent a date. Instead, focus on the original package’s mutability.

Suppose you are running in a multithreaded application, and you have a function that uses a legacy java.util.Date object. This object was passed to your function from elsewhere; your function has no idea where it came from—or where it might be referenced in another thread.

Further, suppose that you are processing that date object while some other thread is modifying the same object. Calls to getYear(), getMonth(), or getDay(), whether performed explicitly or down inside a date formatter or printf(), could easily be intermixed with another thread changing the fields, resulting in an inaccurate date or, worse, an invalid date such as February 31.

These problems are beaten to a pulp by the newer and far superior java.time API. All date classes are immutable, and even the date-formatting classes are thread-safe. As many enthusiastic developers can attest, switching to the new API makes your date-handling code a lot more reliable.

There’s a chapter on how to use the java.time API in my Java Cookbook. Here’s a simple example.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
LocalDate now = LocalDate.now();
LocalDate nextWeek = now.plusDays(7);    // This day next week
LocalDate hastings = LocalDate.of(1066, 10, 14);
Period ago = Period.between(hastings, now);
System.out.printf(
  "The Battle of Hastings was fought %d years and %d months ago\n",
  ago.getYears(), ago.getMonths());

The LocalDate and LocalDateTime classes don’t have a time zone associated with them; the ZonedDate and ZonedDateTime do. As the preceding example shows, the code expects and presents dates in the unambiguous, year-month-day order. There are, of course, formatters that can parse and display dates as mm/dd/yyyy, dd/mm/yyyy, or other formats (based either upon the Locale setting or a custom setting). The default calendar is the Gregorian (Western) calendar; half a dozen other calendars are supported.

The key takeaway for this article is that it is quite possible to make a powerful and comprehensive API in which all the objects provided are immutable and, like the String class, modifying a date returns a new object with the value changed.

Record types

Because many small and medium data classes don’t really need to be mutable at all, Java introduced the first preview of the record type (which was previewed in Java 14 and finalized for Java 16). A record type is an immutable data class that is much less work to create than a regular class; all the common methods and accessors that you need are generated by the compiler.

Objects of a record type can be used like regular objects: constructed with new, passed around, used in collections, and picked apart with getter methods.

Here is a Person record, complete in one line.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
public record Person(String firstName, String lastName, String telNum) {};

After compiling this one-line file and showing its externals with javap, some interesting observations can be made.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
$ javac -d /tmp /tmp/Person.java
$ javap -cp /tmp Person
Compiled from "Person.java"
public final class Person extends java.lang.Record {
  public Person(java.lang.String, java.lang.String, java.lang.String);
  public final java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public java.lang.String firstName();
  public java.lang.String lastName();
  public java.lang.String telNum();
}
$

What does this reveal?

  • A record type is a class that extends Record from java.lang.
  • The compiler has provided the toString(), hashCode(), and equals() methods, which work out of the box; these methods can be overridden if the way they work isn’t right for your application. Other methods can be added as needed.
  • The compiler has provided a constructor and accessors for all fields in the record. They don’t use the traditional getFieldName() pattern; instead, they use the name of each field. There are no partial constructors, as a record is intended to contain a complete representation of the state of a given entity.

You create an instance of this record class by calling its constructor, as follows:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
Person p = new Person("Robin", "Smith", "555-1212");

In JShell you can see the following:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
jshell> public record Person(String firstName, String lastName, String telNum) {};
|  created record Person
jshell> Person p = new Person("Robin", "Smith", "555-1212");
p ==> Person[firstName=Robin, lastName=Smith, telNum=555-1212]
jshell> p.firstName()
$4 ==> "Robin"
jshell>

What if you need to change the contents of a record? It is not possible to add a set method, as shown in this JShell excerpt, since all the fields are final.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
jshell> public record Person(String firstName, String lastName, String telNum) {
  void setTelNum(String telNum) { this.telNum = telNum;}}
|  Error:
|  cannot assign a value to final variable telNum
|  public record Person(String firstName, String lastName, String telNum) {
  void setTelNum(String telNum) { this.telNum = telNum;}}
jshell>

Instead, you need to make a new instance. Suppose the person changed their phone number.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
p = new Person(p.firstName(), p.lastName(), newPhoneNumber);

The notion of re-creating an object may seem frightening (or at least frighteningly inefficient) to those raised on JavaBeans—but remember that almost every release of the JDK improves performance. The engineers who advance the JDK work particularly hard on fast object creation and garbage collection. The result is that creation and re-creation, especially of short-lived data objects such as those that live only inside a method call, are very fast nowadays.

By the way, if the change-phone-number functionality is needed in multiple places in your application, add a copy-and-change factory method, as the String class and the java.time API do.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
$ cat PersonModDemo.java
package structure;

public class PersonModDemo {

  public record PersonMod(String name, String email, String phoneNumber) {

    public PersonMod withPhoneNumber(String number) {
      return new PersonMod(name(), email(), number);
    }
  }

  public static void main(String[] args) {
    PersonMod p = new PersonMod("Robin Smith", "robin_smith@example.com", "555-1234");
    System.out.println(p);
    p = p.withPhoneNumber("555-9876");
    System.out.println(p);
  }
}
$ java PersonModDemo.java
PersonMod[name=Robin Smith, email=robin_smith@example.com, phoneNumber=555-1234]
PersonMod[name=Robin Smith, email=robin_smith@example.com, phoneNumber=555-9876]
$

Conclusion

Immutable objects are now simpler to create compared to JavaBeans with all their setter-and-getter baggage, nearly as easy to work with, and safe against (possibly concurrent) modification.

Although represented by only a few classes in Java’s earliest versions, you’ll find that immutable APIs are increasingly one of the threads being woven into the rich tapestry that is Java’s present and future. Immutable objects lead to improved software reliability and better multithreaded performance. Expect to see more immutable APIs down the road as Java continues to grow and mature.

Dig deeper

Ian Darwin

Ian Darwin is a Java Champion who has done all kinds of development, from mainframe applications and desktop publishing applications for UNIX and Windows, to a desktop database application in Java, to healthcare apps in Java for Android. He’s the author of Java Cookbook and Android Cookbook (both from O’Reilly). He has also written a few courses and taught many at Learning Tree International.


Previous Post

Add a distributed database to your Java application with i.o.cluster

Yuriy Glotanov | 7 min read

Next Post


Quiz yourself: HashSet and TreeSet sources in Java streams

Simon Roberts | 5 min read