equals()
, hashCode()
, and toString()
methods that consider all components of the record, facilitating value-based equality checks and easy data representation.equals()
, hashCode()
, and toString()
, which are typically manually overridden in data-centric classes.In this example, we create a simple class to represent a product in an inventory system.
Before Java 17:
Developers would have to add a lot of boilerplate code for what is just a simple model class.
public class Product { private Long id; private String name; private double price; public Product(Long id, String name, double price) { this.id = id; this.name = name; this.price = price; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Product product = (Product) o; return Double.compare(price, product.price) == 0 && id.equals(product.id) && name.equals(product.name); } @Override public int hashCode() { int result = id.hashCode(); result = 31 * result + name.hashCode(); result = 31 * result + Double.hashCode(price); return result; } @Override public String toString() { return "Product{" + "id=" + id + ", name='" + name + '\'' + ", price=" + price + '}'; } }
After Java 17:
With records everything becomes much simpler.
public record Product(Long id, String name, double price) { }
Making a new record is similar to writing a class, interface etc. but there are some caveats:
record
.public record Product(Long id, String name, double price) { }
public record Product(Long id, String name, double price) { public double applyDiscount(double discount) { return price * (1 - discount); } }
public record Product(Long id, String name, double price) { public Product { if (id == null) throw new IllegalArgumentException("id is null"); if (name == null) throw new IllegalArgumentException("name is null"); if (price < 0) throw new IllegalArgumentException("price is negative"); } }
// ❌ Cannot inherit from final public class DigitalProduct extends Product { }
java.lang.Record
-class which is the only one they’re allowed to extend.// ❌ No extends clause allowed for record public record Product(Long id, String name, double price) extends ProductBase { }
public record Product(Long id, String name, double price) implements Comparable<Product> { @Override public int compareTo(Product o) { return Long.compare(id, o.id); } }
Once a record is defined, using it is straightforward due to the simplicity and automation of the syntax.
Product product = new Product(1L, "Coffee Maker", 99.95);
get
-prefix we’re accustomed to. Instead, the name of the field on its own is used. System.out.println("Product ID: " + product.id()); System.out.println("Product name: " + product.name()); System.out.println("Product price: " + product.price());
product.setPrice(89.99); // ❌ No setter, the fields are final aka immutable
Product product = new Product(1L, "Coffee Maker", 99.95); Product sameProductDifferentObj = new Product(1L, "Coffee Maker", 99.95); System.out.println(product.equals(sameProductDifferentObj)); // ✅ true System.out.println(product == sameProductDifferentObj); // ❌ false
System.out.println(product);
Which of the following statements is true about Java records?
A) Records can extend other classes besides java.lang.Record
.
B) Records can implement any number of interfaces.
C) Records allow mutable fields to be defined within them.
D) Records automatically generate private constructors only.
record ___(String ___, String ___, int ___) {}
Correct the code to ensure it adheres to Java’s record rules about immutability and initialization correctness..
record Instrument(String type, int year) { // Custom constructor for validation public Instrument { if (year < 1500) { throw new IllegalArgumentException("Year must be at least 1500."); } // Mistake in logic, instruments can't be of type "undefined" if (type == null || type.isEmpty()) { type = "undefined"; // This line should be fixed } } }
We’ll model some employees using records!
Employee
-record with some basic components (first and last name, salary and job title).main
-method of a EmployeeApp
-class, make an Employee
-object and have them introduce themselves by printing their full name and job title.Employee
-object directly to test the toString()
.Employee
-object that has the exact same field values as your original employee, just in a new object.equals()
method to compare each employee to the original employee.We’ll model some shapes using records to illustrate polymorphic behaviour.
Shape
-interface that prescribes the methods calcPerimeter(): double
and calcArea(): double
.Rectangle
-record and Circle
-record, providing each with appropriate components.Shape
-interface and its abstract methods.ShapesApp
-class, create a list of rectangles and circles and then loop over each one, and print it as an object, along with its perimeter and area.Correct Answer: B) Records can implement any number of interfaces.
Records are final and cannot be subclassed beyond java.lang.Record
, ensuring that they are simple and consistent in structure. They do not allow mutable fields, as all components of a record are implicitly final. The constructors generated by records are public, not private, and they can implement any number of interfaces, allowing for polymorphic behaviour.
record ___(String ___, String ___, int ___) {}
record Album(String artist, String title, int releaseYear) {}
record Instrument(String type, int year) { // Custom constructor for validation public Instrument { if (year < 1500) { throw new IllegalArgumentException("Year must be at least 1500."); } if (type == null || type.isEmpty()) { type = "undefined"; } } }
record Instrument(String type, int year) { // Custom constructor for validation public Instrument { if (year < 1500) { throw new IllegalArgumentException("Year must be at least 1500."); } if (type == null || type.isEmpty()) { throw new IllegalArgumentException("Type cannot be null or empty."); } } }
The original code attempted to modify the type
field directly, which is not allowed as record components are final. In the corrected version, the record uses a compact constructor with parameters to validate the inputs. It throws an exception if the type
is invalid rather than trying to reset it, adhering to Java’s principles of immutability for records. This ensures that every Instrument
object is correctly initialized with valid data.
public record Employee(String firstName, String lastName, double salary, String jobTitle) { }
public class EmployeeApp { public static void main(String[] args) { Employee employee = new Employee("James", "Barnes", 70_655.32, "Scrum Master"); System.out.printf("My name is %s %s and I am a %s.\n", employee.firstName(), employee.lastName(), employee.jobTitle()); Set<Employee> staff = Set.of( new Employee("Vincent", "Lee", 45_000.0, "Vinyl Master"), new Employee("Ella", "Fitzgerald", 52_000.0, "Quality Control Specialist"), new Employee("Louis", "Armstrong", 56_000.0, "Design Lead"), new Employee("James", "Barnes", 70_655.32, "Scrum Master") ); for (Employee e : staff) { System.out.println(e); System.out.printf("\t%s same as the original employee.\n", e.equals(employee) ? "The": "Not the"); } } }
public interface Shape { double calcPerimeter(); double calcArea(); }
public record Rectangle(double width, double height) implements Shape { public Rectangle { if (width <= 0) throw new IllegalArgumentException("width is negative or zero"); if (height <= 0) throw new IllegalArgumentException("height is negative or zero"); } @Override public double calcPerimeter() { return 2 * (width + height); } @Override public double calcArea() { return width * height; } }
public record Circle(double radius) implements Shape { public Circle { if (radius <= 0) throw new IllegalArgumentException("radius is negative or zero"); } @Override public double calcPerimeter() { return 2 * Math.PI * radius; } @Override public double calcArea() { return Math.PI * (radius * radius); } }
public class ShapesApp { public static void main(String[] args) { List<Shape> shapes = List.of( new Circle(5), new Rectangle(3, 4), new Rectangle(4, 3) ); for (Shape s : shapes) { System.out.println(s); System.out.printf("\tPerimeter: %.2f\n", s.calcPerimeter()); System.out.printf("\tArea: %.2f\n", s.calcArea()); } } }
Using local records within a method can be particularly useful when you need to structure complex data temporarily, especially when dealing with operations that benefit from grouping related data without exposing it outside the method.
public AnimalExhibit findMostVisitedExhibitByMonth(List<AnimalExhibit> exhibits, int month) { // Local record for pairing an exhibit with its visitor count record ExhibitVisits(AnimalExhibit exhibit, int visits) {} return exhibits.stream() .map(exhibit -> new ExhibitVisits(exhibit, findVisitsCountForExhibitByMonth(exhibit, month))) .sorted((e1, e2) -> Integer.compare(e2.visits(), e1.visits())) .map(ExhibitVisits::exhibit) .findFirst().orElseThrow(); }
ExhibitVisits
: This local record ties an AnimalExhibit
object with a visitor count (visits
). The record is scoped within the findMostVisitedExhibitByMonth
-method, ensuring that the pairing of exhibits and visitor statistics is managed internally and only relevant within this method.Java records have been incorporated into various domains within enterprise development, each taking advantage of their streamlined, immutable nature. Here are key areas of use with short examples:
record UserLoginDto(String username, String email) {}
record CustomerView(String firstName, String lastName, long customerNumber) {}
List<Product> products = List.of(new Product(1, "Tea", 1.99));
record OrderCreatedEvent(long orderId, Date creationDate) {}
Stream.of(new Person("John", "Doe")).filter(p -> p.name().equals("John"))
record ServerConfig(String host, int port) {}
Map<Product, ProductDetail> cache = new ConcurrentHashMap<>();
equals
, hashCode
, and toString
.