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.)
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.
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 Date
s. 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:
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.
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:
set
and get
methods for each property, usually backed by a private field.java.io.Serializable
.java.util.EventObject
when its data changes.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.
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 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.
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.
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.
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.
$ 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?
record
type is a class that extends Record
from java.lang
.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.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:
Person p = new Person("Robin", "Smith", "555-1212");
In JShell you can see the following:
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.
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.
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.
$ 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]
$
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.
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