Thursday, 28 July 2022

Java15: Records

Records are classes to define immutable data. This is added as a preview feature in Jdk15.

 

Let’s learn more about records using an example.

 

Without records

Prior to records, we define an immutable class like below.

 

Employee.java

package com.sample.app.recrods;

import java.util.Objects;

public final class Employee {

	private final int id;
	private final String name;

	public Employee(int id, String name) {
		this.id = id;
		this.name = name;
	}

	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	@Override
	public int hashCode() {
		return Objects.hash(id, name);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Employee other = (Employee) obj;
		return id == other.id && Objects.equals(name, other.name);
	}

	@Override
	public String toString() {
		return "Employee [id=" + id + ", name=" + name + "]";
	}

}

 

With records, we can create same Employee immutable class in a compact way.

public record Employee(int id, String name) {
	
}

 

The declaration of record specifies

a.   Name -> Employee

b.   Header -> (int id, String name)

c.    Body -> {}

 

Name specifies the record name, Header specifies the properties of the record.

 

Let’s decompile the Employee class file and explore further

Compile Employee.java class by enabling preview features.

$javac  --release 15 --enable-preview Employee.java
Note: Employee.java uses preview language features.
Note: Recompile with -Xlint:preview for details.

 

Let’s decompile Employee.cass file.

$javap Employee.class                              
Compiled from "Employee.java"
public final class com.sample.app.recrods.Employee extends java.lang.Record {
  public com.sample.app.recrods.Employee(int, java.lang.String);
  public final java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public int id();
  public java.lang.String name();
}

From the above snippet, you can deduce.

a.   Records implicitly extends java.lang.Records class.

b.   For each component or variable in the record header (id, name),

1.   A private final field is defined.

2.   A public access method with the same name as variable name is defined.

c.    Override the implementation of hashCode, equals and toString methods.

 

How to get an instance of Employee record?

Similar to a class, a record is instantiated with the new keyword.

 

Employee emp1 = new Employee(1, "Krishna");

 

RecordDemo.java

package com.sample.app;

import com.sample.app.recrods.Employee;

public class RecordDemo {

	public static void main(String[] args) {
		Employee emp1 = new Employee(1, "Krishna");

		System.out.println("id = %d".formatted(emp1.id()));
		System.out.println("name = %s".formatted(emp1.name()));
	}

}

Output

id = 1
name = Krishna

When you define a record, compiler provide some default implementation to hashCode, equals and toString methods.

You can confirm the same from below application.

 

RecordDemo1.java

package com.sample.app;

import com.sample.app.recrods.Employee;

public class RecordDemo1 {
	
	public static void main(String[] args) {
		Employee emp1 = new Employee(1, "Krishna");
		Employee emp2 = new Employee(2, "Krishna");
		Employee emp3 = new Employee(1, "Krishna");

		System.out.println("id = %d".formatted(emp1.id()));
		System.out.println("name = %s".formatted(emp1.name()));

		System.out.println("\nemp1.hashCode() = %d".formatted(emp1.hashCode()));
		System.out.println("emp1.toString() =  %s".formatted(emp1.toString()));

		System.out.println("\nemp2.hashCode() = %d".formatted(emp2.hashCode()));
		System.out.println("emp2.toString() =  %s".formatted(emp2.toString()));

		System.out.println("\nemp3.hashCode() = %d".formatted(emp3.hashCode()));
		System.out.println("emp3.toString() =  %s".formatted(emp3.toString()));

		System.out.println("\nemp1.equals(emp2) = %s".formatted(emp1.equals(emp2)));
		System.out.println("emp1.equals(emp3) = %s".formatted(emp1.equals(emp3)));
	}
	
}

Output

id = 1
name = Krishna

emp1.hashCode() = 1207521705
emp1.toString() =  Employee[id=1, name=Krishna]

emp2.hashCode() = 1207521736
emp2.toString() =  Employee[id=2, name=Krishna]

emp3.hashCode() = 1207521705
emp3.toString() =  Employee[id=1, name=Krishna]

emp1.equals(emp2) = false
emp1.equals(emp3) = true

How to add validations to the record properties?

we can define a canonical constructor that perform some validations.

 

A canonical constructor  signature is the same as the header, and which assigns each private field to the corresponding argument from the new expression which instantiates the record;

 

Employee.java

package com.sample.app.recrods;

public record Employee(int id, String name) {

	public Employee(int id, String name) {
		if (id <= 0) {
			throw new IllegalArgumentException("id must be > 0");
		}

		if (name == null || name.isBlank()) {
			throw new IllegalArgumentException("name should not be null or blank");
		}

		this.id = id;
		this.name = name;
	}

}

 

Compact canonical constructor

A compact canonical constructor omit the list of formal parameters as they are declared implicitly, and the private fields corresponding to record components cannot be assigned in the body but are automatically assigned to the corresponding formal parameter (this.id = id;) at the end of the constructor.

 

For example, following snippet use a compact canonical constructor that validates its (implicit) formal parameters.

 

Employee.java 

package com.sample.app.recrods;

public record Employee(int id, String name) {

	public Employee {
		if (id <= 0) {
			throw new IllegalArgumentException("id must be > 0");
		}

		if (name == null || name.isBlank()) {
			throw new IllegalArgumentException("name should not be null or blank");
		}
	}

}

 

Constructor rules are different for a class and a record

The rules for constructors are different in a record than in a normal class. A normal class without any constructor declarations is automatically given a default constructor. In contrast, a record without any constructor declarations is automatically given a canonical constructor that assigns all the private fields to the corresponding arguments of the new expression which instantiated the record.

 

Can I overload the constructors in a record?

Yes, you can overload the constructors. But keep in mind that a non-canonical constructor must start with an explicit invocation to a constructor.

package com.sample.app.recrods;

public record Employee(int id, String name) {

	public Employee() {
		this(Integer.MAX_VALUE, "");
	}

	public Employee(int id) {
		this(id, "");
	}

	public Employee(String name) {
		this(Integer.MAX_VALUE, name);
	}

}

 

Can a record is nested in another record?

Yes, a record can be declared top level or nested, and can be generic. If a record is itself nested, then it is implicitly static

package com.sample.app.recrods;

public record Employee(int id, String name) {

	public record Person(){
		
	}

}

 

Can a record define static methods, static blocks and static fields?

Yes, it can.

 

Employee.java

package com.sample.app.recrods;

public record Employee(int id, String name) {
	
	static {
		System.out.println("Employee record loaded");
	}

	private static int count = 0;

	public Employee {
		count++;
	}

	public static int empCount() {
		return count;
	}

}

 

RecordDemo3.java

package com.sample.app;

import com.sample.app.recrods.Employee;

public class RecordDemo3 {
	
	public static void main(String[] args) {
		Employee emp1 = new Employee(1, "Krishna");
		Employee emp2 = new Employee(2, "Ram");
		
		System.out.println("Total employees : "+ Employee.empCount());
	}

}

 

Output

Employee record loaded
Total employees : 2

 

Can a record has instance methods?

Yes, you can define instance methods in a record. A record can explicitly declare public accessor methods which correspond to components, and can also declare other instance methods.


 

Employee.java

package com.sample.app.recrods;

public record Employee(int id, String name) {
	
	static {
		System.out.println("Employee record loaded");
	}

	private static int count = 0;

	public Employee {
		count++;
	}

	public static int empCount() {
		return count;
	}
	
	public String empDetails() {
		return this.toString();
	}

}

 

In the above snippet, empDetails is an instance method.

 

Can a record implement an interface?

Yes, a record can implement interfaces.

public record Employee(int id, String name) implements Serializable {

}

 

Can a record declare nested types?

A record can declare nested types, including nested records

 

What are the limitations on records?

a.   Since a Record extends java.lang.Record implicitly, a record can’t extend any class explicitly.

b.   Since a record is implicitly final, it can’t be abstract.

c.    The implicitly declared fields corresponding to the record components of a record class are final and moreover are not modifiable via reflection (doing so will throw IllegalAccessException).

d.   A record cannot declare native methods.

 

References

https://openjdk.org/jeps/384

https://openjdk.org/jeps/12


  

Previous                                                 Next                                                 Home

No comments:

Post a Comment