Record classes in Java

A quicker and neater way to create Data Transfer Object

Record classes in Java

Hello, devs! Welcome to another article.

Today we begin with the simple task of creating a class that defines an immutable house. Attributes that describe a house are :

  1. Address

  2. Floor Count

Along with this, we also have the requirement to identify whether a house is a skyscraper or not(Perhaps, Tony Stark lives here) which we derive from floor count(floor count greater than or equal to 30)

To create a basic class for this as per regular java syntax, here's the code we'll generally go with:

public class House {
        private final String address;
        private final int floors;

        public House(String address, int floors) {
            this.address = address;
            this.floors = floors;
        }

        public String getAddress() {
            return address;
        }

        public int getFloors() {
            return floors;
        }

        @Override
        public String toString() {
            return "House{" +
                    "address='" + address + '\'' +
                    ", floors=" + floors +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            House house = (House) o;
            return floors == house.floors && Objects.equals(address, house.address);
        }

        @Override
        public int hashCode() {
            return Objects.hash(address, floors);
        }

        public boolean isSkyScraper() {
            return this.floors >= 30;
        }
    }

This piece of code lets you create a class that lets you instantiate the class via a parameterized constructor and get with public accessors. Along with that, it also implements toString, hashCode & equals methods.

Now, how does the new feature of "Record" classes fit in?

Record classes were introduced as a preview feature in Java 14 & rolled out as a full-fledged feature in Java 16. This feature aims to reduce the code that developer has to write every time they have to create a class just to store and transport values(DTO). So this entire above code can simply be converted into :

record House(String address, int floors){}

This one line of code has almost everything from the code that we wrote without record API except one thing, the logic to check if it's a skyscraper or not.

To achieve that, we can simply define that method inside the class like we regularly do.

record House(String address, int floors){
    public boolean isSkyScraper() {
        return this.floors >=30;
    }
}

And to use this class, we go through standard object creation syntax.

public static void main(String[] args) {
    House house = new House("Somewhere on Earth", 25);

    System.out.println(house);
    System.out.println("Address is : "+house.address());
}

/*
House[address=Somewhere on Earth, floors=25]
Address is : Somewhere on Earth
*/

To customize the class further as per our requirements, we can keep on adding methods to either override the existing functionality or add more functionality. For example, let's say we want to validate the address and floors while instantiating the class. This can easily be achieved by manually defining a constructor with exactly the same signature as the record class's

record House(String address, int floors){

        public House(String address, int floors) {
            if(address == null || address.length() <= 10 || floors < 1) {
                throw new IllegalArgumentException("Improper Inputs");
            }
            this.address = address;
            this.floors = floors;
        }
        public boolean isSkyScraper() {
            return this.floors >=30;
        }
    }

Now as soon as you try to do this,

House house = new House("Addr", 0);

You get this,

Exception in thread "main" java.lang.IllegalArgumentException: Improper Inputs

To sum up, a few things that a record class can do out of the box or with just the bare minimum code:

  1. Generates private & final fields with the arguments defined in the class declaration

  2. Generates a canonical constructor with the arguments

  3. Generates public accessors with the same name as arguments

  4. Generates toString, hashCode & equals methods

Now, what can we do more with record classes?

Almost everything a regular class can do. For instance, we can write custom methods with business logic like the one created above to check if the house is a skyscraper or not.

We can also create static fields, initializer blocks, and methods inside a record class. Like this:

record House(String address, int floors){

        static String defaultBuildingMaterial;

        static {
            defaultBuildingMaterial = "Concrete";
        }
        public House(String address, int floors) {
            if(address == null || address.length() <= 10 || floors < 1) {
                throw new IllegalArgumentException("Improper Inputs");
            }
            this.address = address;
            this.floors = floors;
        }
        public boolean isSkyScraper() {
            return this.floors >=30;
        }
    }

One major thing to remember about record classes is that they don't allow instance variables or instance initializers. Only static fields and initializers are allowed.

It is also possible to have nested record classes.

record House(String address, int floors){

        /* Nested record class to hold geographical data for a house*/
        record Geography(String country, String state, String city){}

        static String defaultBuildingMaterial;

        static {
            defaultBuildingMaterial = "Concrete";
        }
        public House(String address, int floors) {
            if(address == null || address.length() <= 10 || floors < 1) {
                throw new IllegalArgumentException("Improper Inputs");
            }
            this.address = address;
            this.floors = floors;
        }
        public boolean isSkyScraper() {
            return this.floors >=30;
        }
    }

Furthermore, it's also possible to have generic record classes.

record Villa<H extends House> (H house){}

To wrap it up, a record class is just a new syntax to shorten the code for classes used to transport data with immutability. Although it does sound like a mission against boilerplate code, JEP 395 clearly states that:

While records do offer improved concision when declaring data carrier classes, it is not a goal to declare a "war on boilerplate". In particular, it is not a goal to address the problems of mutable classes which use the JavaBeans naming conventions.

That should settle any doubts about that.

With this, we wrap this article up. Surely there must be a couple more things to figure out about Record classes. But this should give a fair idea of what they are and how they can be used.

Until next time :)