Records as a logical conclusion of Immutable Classes

jjuanbj

Juan Jiménez

Posted on May 2, 2023

Records as a logical conclusion of Immutable Classes

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

Here is the output:

Car name: Honda
Car model: Civic
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

Here is the output, in which we can see a logical malfunction:

Car name: Honda
Car model: Civic
Car name: Toyota
Car model: Corolla
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

Remember, the output is the same:

Car name: Honda
Car model: Civic
Car name: Toyota
Car model: Corolla
Enter fullscreen mode Exit fullscreen mode

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){}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
jjuanbj
Juan Jiménez

Posted on May 2, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related