Records, added in JDK 16 (JEP 395), are designed to address difficulties around defining data carriers in Java. In this we will look at how Records help when transforming data within a method.
Defining even a simple data carrier before records often required dozens of lines of code. A data carrier would need; fields, a constructor, accessors, equals(), hashCode(), and often toString(), for printing of the contents. This problem can been seen in this below example:
String firstName1 = "Billy";
String lastName1 = "Korando";
String title1 = "Java Developer Advocate";
String twitterHandle1 = "@BillyKorando";
String firstName2 = "Sharat";
String lastName2 = "Chander";
String title2 = "Java Developer Advocate";
String twitterHandle2 = "@Sharat_Chander";
class Person{
	private String firstName;
	private String lastName;
	private String title;
	private String twitterHandle;
	public Person(String firstName, String lastName, String title, String twitterHandle) {
		this.firstName = firstName;
		this.lastName = lastName;
		this.title = title;
		this.twitterHandle = twitterHandle;
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
		result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
		result = prime * result + ((title == null) ? 0 : title.hashCode());
		result = prime * result + ((twitterHandle == null) ? 0 : twitterHandle.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (firstName == null) {
			if (other.firstName != null)
				return false;
		} else if (!firstName.equals(other.firstName))
			return false;
		if (lastName == null) {
			if (other.lastName != null)
				return false;
		} else if (!lastName.equals(other.lastName))
			return false;
		if (title == null) {
			if (other.title != null)
				return false;
		} else if (!title.equals(other.title))
			return false;
		if (twitterHandle == null) {
			if (other.twitterHandle != null)
				return false;
		} else if (!twitterHandle.equals(other.twitterHandle))
			return false;
		return true;
	}
	@Override
	public String toString() {
		return "Person [firstName=" + firstName + ", lastName=" + lastName + ", title=" + title
				+ ", twitterHandle=" + twitterHandle + "]";
	}
}
var persons = Stream.of(new Person(firstName1, lastName1, title1, twitterHandle1), 
new Person(firstName2, lastName2, title2, twitterHandle2));
persons.forEach(System.out::println);
In the above example the meaning of the what the method is doing is lost because of all the cruft associated with defining a data carrier in Java. With a Records a data carrier can be defined within a single line:
String firstName1 = "Billy";
String lastName1 = "Korando";
String title1 = "Java Developer Advocate";
String twitterHandle1 = "@BillyKorando";
String firstName2 = "Sharat";
String lastName2 = "Chander";
String title2 = "Java Developer Advocate";
String twitterHandle2 = "@Sharat_Chander";
record Person(String firstName, String lastName, String title, String twitterHandle) {}
var persons = Stream.of(new Person(firstName1, lastName1, title1, twitterHandle1),
		new Person(firstName2, lastName2, title2, twitterHandle2));
persons.forEach(System.out::println);
The ability to define a data carrier within a single line does come with some tradeoffs. There is less freedom allowed in the defining of a data carrier, but in return a canonical constructor, accessors, equals(), hashCode(), and toString(), are automatically generated by the Java compiler. There are a few other important restrictions, which you can read here.
An explicit declaration of a generated method is allowed, like in this example with toString():
String firstName1 = "Billy";
String lastName1 = "Korando";
String title1 = "Java Developer Advocate";
String twitterHandle1 = "@BillyKorando";
String firstName2 = "Sharat";
String lastName2 = "Chander";
String title2 = "Java Developer Advocate";
String twitterHandle2 = "@Sharat_Chander";
record Person(String firstName, String lastName, String title, String twitterHandle) {
	public String toString() {
		return lastName + ", " + firstName + " twitter handle: " + twitterHandle + " job title: " + title;
	}
}
var persons = Stream.of(new Person(firstName1, lastName1, title1, twitterHandle1),
		new Person(firstName2, lastName2, title2, twitterHandle2));
persons.forEach(System.out::println);
Additional methods can also be added to a record class like here with toJSON():
String firstName1 = "Billy";
String lastName1 = "Korando";
String title1 = "Java Developer Advocate";
String twitterHandle1 = "@BillyKorando";
String firstName2 = "Sharat";
String lastName2 = "Chander";
String title2 = "Java Developer Advocate";
String twitterHandle2 = "@Sharat_Chander";
record Person(String firstName, String lastName, String title, String twitterHandle) {
	public String toJSON() {
		return """
				{
					"firstName" : "%s",
					"lastName" : "%s",
					"title" : "%s",
					"twitterHandle" : "%s"
				}
			   """.formatted(firstName, lastName, title, twitterHandle);
	}
}
var persons = Stream.of(new Person(firstName1, lastName1, title1, twitterHandle1),
		new Person(firstName2, lastName2, title2, twitterHandle2));
persons.forEach(p -> System.out.println(p.toJSON()));
Happy Coding!