sip-of-java

View the Project on GitHub wkorando/sip-of-java

Local Records

Video Script

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 Data Carriers Before Records

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);

Defining a Data Carrier with Records

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);

Tradeoffs with Records

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.

Explicitly Defining Generated Methods

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);

Defining Additional Methods

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!