Class vs Record: Difference between class and record in Java

Houssemmedine Drissi
6 min readNov 29, 2022

1. Introduction

What is immutability?

Immutability is the concept of an object that cannot change after creation. This is a useful concept because it helps you keep data integrity. It ensures that we don’t accidentally change or corrupt data.

Context

Passing immutable data between objects is one of the most common, but mundane tasks in many Java applications.

This required the creation of a class with boilerplate fields and methods, which were susceptible to trivial mistakes and muddled intentions.

With the recent releases of Java (+ 14 ), we can now use records to remedy these problems.

In this article, we’ll look at the purpose of records, difference between Classes vs Records and record basics..

2. Purpose

Commonly, we write classes to simply hold data, such as database results, query results, or information from a service.

In many cases, this data is immutable, since immutability ensures the validity of the data without synchronization.

To accomplish this, we create data classes with the following:

  1. private, final field for each piece of data
  2. getter for each field
  3. public constructor with a corresponding argument for each field
  4. equals method that returns true for objects of the same class when all fields match
  5. hashCode method that returns the same value when all fields match
  6. toString method that includes the name of the class and the name of each field and its corresponding value

For example, we can create a simple Citizen data class with a name and country:

public class Citizen{
private final String name;
private final String country;
public Citizen(String name, String country) {
this.name = name;
this.address = country;
}
@Override
public int hashCode() {
return Objects.hash(name, country);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof Citizen)) {
return false;
} else {
Citizen other = (Citizen) obj;
return Objects.equals(name, other.name)
&& Objects.equals(country, other.country);
}
}
@Override
public String toString() {
return "Citizen [name=" + name + ", country=" + country + "]";
}
// standard getters
}

While this accomplishes our goal, there are two problems with it:

  1. There’s a lot of boilerplate code
  2. We obscure the purpose of our class: to represent a Citizen with a name and country

In the first case, we have to repeat the same tedious process for each data class, monotonously creating a new field for each piece of data; creating equals, hashCode, and toString methods; and creating a constructor that accepts each field.

While IDEs can automatically generate many of these classes, they fail to automatically update our classes when we add a new field. For example, if we add a new field, we have to update our equals method to incorporate this field.

In the second case, the extra code obscures that our class is simply a data class that has two String fields, name and country.

A better approach would be to explicitly declare that our class is a data class.

When to use a record over a class?

Use a record when an object’s only purpose is to contain public data. On other hand, use a class if your object has unique logic. Classes are mutable so even if they have the same data, doesn’t mean they are the same.

For example, if we think about a class that represents a Citizen . Two citizens on a screen can look the same, and have the same name and country, but they are not the same :

Classes have the same data, but they are not the same.

On the other hand, if we think of records as a just bag of data, we only care about data being the same:

Records have the same data, so they are considered equal.

3. Class vs Record

As of JDKs (+14) , we can replace our repetitious data classes with records. Records are immutable data classes that require only the type and name of fields.

So ,the main difference between class and record type in Java is that a record has the main purpose of storing data, while a class defines responsibility. Records are immutable, while classes are not.

Simply put, a class is an OOP concept that wraps data with functionality, while a record represents a set of data.

Other differences between class and record type include:

  • We define records using the record keyword instead of the class keyword.
  • Records should not have any state changes after instantiation, while classes change properties.
  • We create new records from existing ones when we want to change state. With classes, we modify the existing ones.

4. Basics

The equals, hashCode, and toString methods, as well as the private, final fields and public constructor, are generated by the Java compiler.

To create a Citizen record, we’ll use the record keyword:

public record Citizen (String name, String country) {}

Constructor

Using records, a public constructor, with an argument for each field, is generated for us.

In the case of our Citizen record, the equivalent constructor is:

public Citizen(String name, String conutry) {
this.name = name;
this.country = country;
}

This constructor can be used in the same way as a class to instantiate objects from the record:

Citizen person = new Citizen("drissi houcem", "tunisia");

equals

Additionally, an equals method is generated for us.

This method returns true if the supplied object is of the same type and the values of all of its fields match:

@Test
public void givenSameNameAndCountry_whenEquals_thenCitizensEqual() {
String name = "drissi houcem";
String country = "tunisia";

Citizen person1 = new Citizen(name, country);
Citizen person2 = new Citizen(name, country);

assertTrue(person1.equals(person2));
}

If any of the fields differ between two Citizen instances, the equals method will return false.

hashCode

Similar to our equals method, a corresponding hashCode method is also generated for us.

Our hashCode method returns the same value for two Person objects if all of the field values for both objects match (barring collisions due to the birthday paradox):

@Test
public void givenSameNameAndCountry_whenHashCode_thenCitizensEqual() {
String name = "drissi houcem";
String country = "tunisia";

Citizen person1 = new Citizen(name, country);
Citizen person2 = new Citizen(name, country);

assertEquals(person1.hashCode(), person2.hashCode());
}

The hashCode value will differ if any of the field values differ.

toString

Lastly, we also receive a toString method that results in a string containing the name of the record, followed by the name of each field and its corresponding value in square brackets.

Therefore, instantiating a Person with a name of “drissi houcem” and a country of “tunisia” results in the following toString result:

Citizen[name=drissi houcem, country=tunisia]

Static Variables & Methods

As with regular Java classes, we can also include static variables and methods in our records.

We declare static variables using the same syntax as a class:

public record Citizen(String name, String country) {
public static String UNKNOWN_COUNTRY = "Unknown";
}

Likewise, we declare static methods using the same syntax as a class:

public record Citizen(String name, String country) {
public static Citizen withoutCountry(String country) {
return new Citizen("withoutCountry", country);
}
}

We can then reference both static variables and static methods using the name of the record:

Citizen.UNKNOWN_COUNTRY
Citizen.withoutCountry("tunisia");

5. Conclusion

Using records with their compiler-generated methods, we can reduce boilerplate code and improve the reliability of our immutable classes.

6. Ressources

--

--