Java records – an efficient way to manage data classes in modern programming

Java has often been criticized in the past for the amount of code that must be written to get a program to do something. The biggest complaints were directed at the data classes. Oracle was aware of this and began working to make Java acquire the attributes of brevity and developer friendliness in addition to robustness and verbosity. In Java 14[JEP-359], the authors provided a preview of how such data classes could be implemented, and after incorporating feedback from the Java community, they brought out a new extension to the Java language in Java 16[JEP-395], Java Records.

Java records - an efficient way to manage data classes in modern programming
Java records - an efficient way to manage data classes in modern programming

V článku sa dozvieš:

    What is Java Record?

    Podobne ako Enum, Record je tiež špeciálny typ triedy v Jave. Records (záznamy) sú určené na zjednodušenie vytvárania nemenných dátových objektov, často označovaných aj ako dátové nosiče. Hlavný rozdiel medzi klasickou triedou a záznamom je v tom, že záznam sa snaží odstrániť celý sprievodný kód, ktorý je potrebný napísať pri nastavení dát alebo pri ich získavaní v inštancii triedy. Java záznam túto zodpovednosť deleguje na Java kompilátor, ktorý automaticky poskytuje stručné implementácie bežných metód ako sú: konštruktor, prístup k členom triedy (getter metódy), hashCode(), equals() a toString() metóda.

    In a nutshell, this important functionality of Java records has brought the ability to define immutable data more concisely, while helping to reduce the amount of repetitive code, so developers can focus mainly on programming business logic.

    The best way to understand the benefits of this enhancement is by example. Let’s now look together at how we would define a data aggregate without and with Java Record. Let’s create a simple data class Book, with attributes title and price.

    Example: data class Book without using Record

    In this example, even though the Book class contains only two fields (name and price), a lot of repetitive code (constructor, getters, toString(), equals(), and hashCode()) needs to be written to ensure that the object works properly.

    import java.util.Objects;
    
    public class Book {
        private final String name;
        private final double price;
    
        public Book(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        public String getName() {
            return name;
        }
    
        public double getPrice() {
            return price;
        }
    
        @Override
        public String toString() {
            return "Book {name = '" + name + "', price = " + price + "}";
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Book book = (Book) o;
            return Double.compare(book.price, price) == 0 && name.equals(book.name);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, price);
        }
    }

    Example: data class Book using Record

    With the introduction of the record keyword, the same data class can be defined much more concisely.

    public record Book(String name, double price) {}

    This one-line declaration does essentially the following:

    • defines a Book class with two final attributes: name and price,
    • automatically generates the constructor, getter methods, toString(), equals(), and hashCode() .

    If we look at the byte-code we can notice a few details:

    • The compiler replaced the record keyword with the word class.
    • The compiler declared the class as final. This means that this class cannot be extended. It also means that it cannot be inherited and is immutable by nature.
    • The converted class inherits from lang.Record. All records are a subclass of the Record class defined in the java.lang.
    • There is a parameterized constructor added by the compiler.
    • The compiler automatically generated the toString(), hashCode() and equal() methods.
    • The compiler has added methods to access class attributes. They match the attribute names and do not contain any prefixes such as get or set.
    Book class created by the compiler from a record
    Book class created by the compiler from a record

    The usage in the Main class is similar to what we are used to from using classic JavaBean classes. However, we have to unlearn the use of getter methods, since the record class does not contain them.

    public class Main {
        public static void main(String[] args) {
            Book book1 = new Book("Effective Java", 47.38);
            System.out.println(book1.name());
            System.out.println(book1.price());
            System.out.println(book1);
            Book book2 = new Book("Effective Java", 47.38);
            System.out.println(book1.equals(book2));
        }
    }

    The output of this program is as follows:

    Program output - Book data class using Record

    Benefits of using Java Records

    In the previous example, we saw how by defining a record class using a single line, we were able to easily implement a simple data class. So let’s summarize the benefits of using Java Records.

    1. Reducing the amount of code: As has been shown, the use of records eliminates the need to manually write many of the accompanying but necessary methods. These methods have not disappeared, the compiler handles them.
    2. Immutability: records are immutable by design. Once an object is created, its fields cannot be changed. This makes records ideal for scenarios where immutability is a requirement (e.g., domain entities in functional programming or distributed systems).
    3. Automated access methods: records automatically generate access methods (getters) for each field. Instead of the typical getFieldName(), however, these methods have the same names as the attributes. For example, in Record Book, we use the book.name() and book.price() methods to access the data.
    4. Built-in canonical constructor: when record is defined, a canonical constructor is automatically created that initializes all fields in the record.
    5. Easy extension with new attributes: changes in the logic do not require updating the standard methods, which are generated automatically and correctly by the compiler. The new attributes are taken care of by the compiler, which pregenerates the code and creates a new class.
    6. Error reduction: the common errors that used to occur, for example, when defining the equals() method, that is, defining the logic when two object instances are the same, are a thing of the past. The correctness of the code automatically generated by the compiler can be relied upon.

    When to use Java Records

    In general, we can use records in any situation where we need to declare a simple data container with record immutability properties and want to use automatically generated methods. We will mention a few usage scenarios where Java records come in handy:

    Data transfer objects (DTOs)

    We can use records to declare simple data transfer objects that contain data. This is useful when transferring data between different layers of an application, such as between the service layer and the database layer.

    Configuration objects

    Entries can be used to declare configuration objects that contain a set of configuration properties for an application or module. These objects typically have immutable properties, making them thread-safe.

    Web service answers

    When creating a REST API, it is common to return data in JSON or XML form. In such cases, we want to define a simple data structure that represents the API response. Records are ideal for this, as they allow you to define a lightweight and immutable data structure that can be easily serialized into JSON or XML.

    Test data

    When writing unit tests, it is often necessary to create test data for a specific test scenario. In such cases, records are ideal for this because test data is usually static and we can create more complex test suites with a minimal amount of code.

    When to avoid using Java Records

    Despite their advantages, records do not fully replace the original JavaBean classes. Therefore, if we need:

    • Changing data after creation
      If the data needs to be changed after creation, you will need to delete the old instance and create a new instance of the class or use a standard class instead of the record class.
    • Comprehensive business logic
      Although Java record can contain methods with business logic, it is recommended to maintain records as data entities where possible and implement the business logic in separate classes that then work with the records. However, if your class requires complex logic beyond data storage, traditional classes will be a better choice.

    Conclusion

    Java records provide us with an elegant way to model immutable data classes with a minimum of code. It is particularly ideal for classes that are primarily used to store data, allowing developers to write cleaner and more maintainable code. By using records, we delegate the amount of code that needs to be written to the compiler. However, for scenarios involving mutable data or complex behavior, we still have traditional classes, which remain a preferable choice in such cases.

    About the author

    Jozef Wagner

    Java Developer Senior

    I have been programming in Java for more than 10 years, currently I am working in msg life Slovakia as a senior Java programmer and I help customers to implement their requirements into Life Factory insurance software. In my free time I like to relax in the mountains or play a good computer game.

    Let us know about you