Records as a logical conclusion of Immutable Classes
Juan Jiménez
Posted on May 2, 2023
Record
type is an addition to Java 17, which has some features that help us when we code and are useful when we're thinking about whether a new class should have setters, or factory methods.
In Joshua Bloch's book Effective Java he writes about the importance of immutable classes since it is not necessary for most of us to create classes with setters. He said:
An (...) advantage of static factory methods is that, unlike constructors, they are not forced to create a new object each time they are called. This allows immutable classes to use pre-constructed instances, or to cache instances as they’re constructed, and dispense them repeatedly to avoid creating unnecessary duplicate objects.
We can see an example of misuse a mutable class, with setters, below:
First we create the typical Car
class:
public class Car {
private String name;
private String model;
public String getName() {
return name;
}
public String getModel(){
return model;
}
public void setName(String name){
this.name = name;
}
public void setModel(String model) {
this.model = model;
}
}
Next we create the main class that instatiates the Car
class:
public class CarFactory {
public static void main(String[] args) {
Car hondaCivic = new Car();
hondaCivic.setName("Honda");
hondaCivic.setModel("Civic");
System.out.println("Car name" + hondaCivic.getName());
System.out.println("Car model" + hondaCivic.getModel());
}
}
Here is the output:
Car name: Honda
Car model: Civic
Although it looks good, there is a security flaw in this code, because even though the object name is hondaCivic
and we set the corresponding parameters, we can change the values and get an invalid result. For example:
public class CarFactory {
public static void main(String[] args) {
Car hondaCivic = new Car();
hondaCivic.setName("Honda");
hondaCivic.setModel("Civic");
System.out.println("Car name: " + hondaCivic.getName());
System.out.println("Car model: " + hondaCivic.getModel());
hondaCivic.setName("Toyota");
hondaCivic.setModel("Corolla");
System.out.println("Car name: " + hondaCivic.getName());
System.out.println("Car model: " + hondaCivic.getModel());
}
}
Here is the output, in which we can see a logical malfunction:
Car name: Honda
Car model: Civic
Car name: Toyota
Car model: Corolla
hondaCivic
is an object created from a mutable class, which means that after it is created, we can change the properties (or attributes) of the object. This is not bad by itself, but in large applications it can be very complex when dealing with these types of classes.
The alternative of mutable classes are, of course, immutable classes. This means that, when instantiated, this object represents the same collection of properties all the time.
The are many forms of creating immutable classes, here is a very simple one, by changing the Car
class:
public class Car {
// The default constructor is private
// preventing multiple instantiation
private Car () {}
private String name;
private String model;
public String getName() {
return name;
}
public String getModel(){
return model;
}
// A static method who return the desired
// car object
public static Car createHondaCivic(){
Car car = new Car();
car.setName("Honda");
car.setModel("Civic");
return car;
}
public static Car createToyotaCorolla(){
Car car = new Car();
car.setName("Toyota");
car.setModel("Corolla");
return car;
}
// There is no need of public setters anymore
private void setName(String name){
this.name = name;
}
private void setModel(String model) {
this.model = model;
}
}
As we said erlier, instead of letting CarFactory
set any type of properties, or create any intance with mutable condition, we restrict it to only two types of Car
. Remmember that this is a very simple example. We can use this pattern without any problem, but we'll see a more flexible one later.
Next, we'll look the refactored CarFactory
class:
public class CarFactory {
public static void main(String[] args) {
Car hondaCivic = Car.createHondaCivic();
System.out.println("Car name: " + hondaCivic.getName());
System.out.println("Car model: " + hondaCivic.getModel());
Car toyotaCorolla = Car.createToyotaCorolla();
System.out.println("Car name: " + toyotaCorolla.getName());
System.out.println("Car model: " + toyotaCorolla.getModel());
}
}
Do we need a Honda Civic object? We use the createHondaCivic()
method. Do we need a Toyota Corolla object? We use the createToyotaCorolla()
method. As we said, this pattern is useful in some cases.
But what happen if we need to set the properties of Car
at runtime. Or if we simply need to set some of these properties from another class? It is a headache to create static methods inside the Car
class every time we need a new type of properties of this class. And it's imposible (without reflection) if we need to do this at runtime. Then we will make another modification to the Car
class:
public class Car {
private Car () {}
private String name;
private String model;
public String getName() {
return name;
}
public String getModel(){
return model;
}
// Using one static method to set properties
public static Car createCar(String name, String model){
Car car = new Car();
car.setName(name);
car.setModel(model);
return car;
}
private void setName(String name){
this.name = name;
}
private void setModel(String model) {
this.model = model;
}
}
Here we use the static method createCar()
to get an immutable class. We can test this in CarFactory
:
public class CarFactory {
public static void main(String[] args) {
Car hondaCivic = Car.createCar("Honda", "Civic");
System.out.println("Car name: " + hondaCivic.getName());
System.out.println("Car model: " + hondaCivic.getModel());
Car toyotaCorolla = Car.createCar("Toyota", "Corolla");
System.out.println("Car name: " + toyotaCorolla.getName());
System.out.println("Car model: " + toyotaCorolla.getModel());
}
}
Remember, the output is the same:
Car name: Honda
Car model: Civic
Car name: Toyota
Car model: Corolla
Now we can have all the power of an immutable class, where no one can change the properties of the objects after instantiation. This is called Singleton pattern when:
- The default constructor is private, which prevents it from being accessed outside of the class
- No public setters, which prevent changing properties after instantiation
- Instantiation is handled by a static method, which returns an immutable object
There is the Builder pattern where the name of the properties are explicit at instantiation time. But this is out of the scope of this article. Although it is good that you to look for more information about it.
Ok, but what is a record
and what is its connection to immutable clases? Well, the thing is that since Java 17 we can create a record
like this:
record Car(String name, String model){}
Record's properties are immutable. That means you don't need to create a private constructor and then create a static method to set values. You can instantiate the new Car
class record
below:
public class CarFactory {
public static void main(String[] args) {
Car hondaCivic = Car("Honda", "Civic");
System.out.println("Car name: " + hondaCivic.name());
System.out.println("Car model: " + hondaCivic.model());
Car toyotaCorolla = Car("Toyota", "Corolla");
System.out.println("Car name: " + toyotaCorolla.name());
System.out.println("Car model: " + toyotaCorolla.model());
}
}
The output is the same as before, with immutable objects but with less code.
This is an example of how a versatile design pattern can inspire design changes in a programming language like Java.
Posted on May 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.