Make your Java Code Better Use java.util.Optional instead of null By using java.util.Optional you will force
java.util.Optional
instead of null
By using java.util.Optional
you willforceclients to check existence of the value.
Consider getBeer(...)
method below, the caller of the method expects to receive a Beer
object and it’s not clear from the method API that the Beer
can be null
. The caller might forget to add a null
check and will potentially receive NullPointerException
which is a programmer error, by the way.
public Beer getBeer(Customer customer) {
return customer.age < 18
? null
: this.beerCatalogue.get(0);
}
public Optional getBeer(Customer customer) {
return customer.age < 18
? empty()
: Optional.of(this.beerCatalogue.get(0));
}
null
Motivation is the same as above - we should not rely on null
public List getBeerCatalogue(Customer customer) {
return customer.age < 18
? null
: this.beerCatalogue;
}
public List getBeerCatalogue(Customer customer) {
return customer.age < 18
? emptyList()
: this.beerCatalogue;
}
var
Type inference reduces the amount of boilerplate code and reduces cognitive complexity, which leads to better readability.
static FieldMapper fieldMapper(FieldDescriptor fieldDescriptor,
SchemaDefinition schemaDefinition) {
ValueGetter valueGetter = valueGetter(schemaDefinition, fieldDescriptor);
return (dynamicMessageBuilder, row) -> {
Iterable iterable = (Iterable) valueGetter.get(row);
for (Object entry : iterable) {
dynamicMessageBuilder.addRepeatedField(fieldDescriptor, entry);
}
};
}
static FieldMapper fieldMapper(FieldDescriptor fieldDescriptor,
SchemaDefinition schemaDefinition) {
var valueGetter = valueGetter(schemaDefinition, fieldDescriptor);
return (dynamicMessageBuilder, row) -> {
var iterable = (Iterable) valueGetter.get(row);
for (var entry : iterable) {
dynamicMessageBuilder.addRepeatedField(fieldDescriptor, entry);
}
};
}
final
Making local variables final
hints to a programmer that the variable can’t be reassigned, which generally leads to better code quality and helps avoid bugs.
static FieldMapper fieldMapper(FieldDescriptor fieldDescriptor,
SchemaDefinition schemaDefinition) {
var valueGetter = valueGetter(schemaDefinition, fieldDescriptor);
return (dynamicMessageBuilder, row) -> {
var iterable = (Iterable) valueGetter.get(row);
for (var entry : iterable) {
dynamicMessageBuilder.addRepeatedField(fieldDescriptor, entry);
}
};
}
static FieldMapper fieldMapper(FieldDescriptor fieldDescriptor,
SchemaDefinition schemaDefinition) {
final var valueGetter = valueGetter(schemaDefinition, fieldDescriptor);
return (dynamicMessageBuilder, row) -> {
final var iterable = (Iterable) valueGetter.get(row);
for (final var entry : iterable) {
dynamicMessageBuilder.addRepeatedField(fieldDescriptor, entry);
}
};
}
Static imports make code less verbose and hence more readable.
Please note, there is one edge case to this rule - There are a bunch of static methods in Java ( List.of()
, Set.of()
, Map.of()
etc.) static importing which would harm code quality, making it ambiguous. So, using this rule, always ask yourself -Does this static import make code more readable?
public static List fieldGetters(SchemaDefinition schemaDefinition,
FieldName fieldName,
FieldDescriptor fieldDescriptor) {
final var schemaField = SchemaUtils.findFieldByName(schemaDefinition, fieldName)
.orElseThrow(SchemaFieldNotFoundException::new);
if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
return schemaField.getFields().stream()
.flatMap(it -> fieldGetters(schemaDefinition, it.getName(), it.getDescriptor()))
.collect(Collectors.toList());
}
return Collections.emptyList();
}
public static List fieldGetters(SchemaDefinition schemaDefinition,
FieldName fieldName,
FieldDescriptor fieldDescriptor) {
final var schemaField = findFieldByName(schemaDefinition, fieldName)
.orElseThrow(SchemaFieldNotFoundException::new);
if (fieldDescriptor.getJavaType() == MESSAGE) {
return schemaField.getFields().stream()
.flatMap(it -> fieldGetters(schemaDefinition, it.getName(), it.getDescriptor()))
.collect(toList());
}
return emptyList();
}
The same as above, it makes code more readable.
public SchemaDefinition.GraphView makeSchemaDefinitionGraph() {
final var rootNode = new SchemaDefinition.RootNode();
final var messageNode1 = new SchemaDefinition.MessageNode.MessageNodeBuilder()
.withHeader("message-header-1")
.withBody("message-body-1")
.build();
final var messageNode2 = new SchemaDefinition.MessageNode.MessageNodeBuilder()
.withHeader("message-header-2")
.withBody("message-body-2")
.build();
rootNode.addNode(messageNode1);
rootNode.addNode(messageNode2);
return rootNode.asGraph();
}
public GraphView makeSchemaDefinitionGraph() {
final var rootNode = new RootNode();
final var messageNode1 = new MessageNodeBuilder()
.withHeader("message-header-1")
.withBody("message-body-1")
.build();
final var messageNode2 = new MessageNodeBuilder()
.withHeader("message-header-2")
.withBody("message-body-2")
.build();
rootNode.addNode(messageNode1);
rootNode.addNode(messageNode2);
return rootNode.asGraph();
}
Having a particular code style and using it across the codebase reduces cognitive complexity, meaning that code is easier to read and understand.
public void processUserData(String name, int age, String address, double salary, boolean isEmployed, String occupation) {
//...
}
// or
public void processUserData(
String name, int age, String address, double salary, boolean isEmployed, String occupation
) {
//...
}
// or
public void processUserData(String name, int age,
String address, double salary, boolean isEmployed, String occupation
) {
//...
}
// or
public void processUserData(String name, int age, String address, double salary,
boolean isEmployed, String occupation
) {
//...
}
public void processUserData(String name,
int age,
String address,
double salary,
boolean isEmployed,
String occupation) {
//...
}
// or
public void processUserData(
String name,
int age,
String address,
double salary,
boolean isEmployed,
String occupation) {
//...
}
record
Immutable classes are easier to design, implement, and use than mutable ones. They are less prone to error and are more secure. With immutable objects, you don't have to worry about synchronisation or object state (Was the object initialised or not?).
To make class immutable:
final
constructor
/ builder
that will initialize all fieldsnull
) use java.util.Optional
Collections.unmodifiableList(...)
etc)public class User {
private String name;
private int age;
private String address;
private List claims;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public List getClaims() {
return claims;
}
public void setClaims(List claims) {
this.claims = claims;
}
// ...
}
public class User {
private final String name;
private final int age;
private final String address;
private final List claims;
public User(String name, int age, String address, List claims) {
this.name = requireNonNull(name);
this.age = requirePositive(age);
this.address = requireNonNull(address);
this.claims = List.copyOf(requireNonNull(claims));
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getAddress() {
return address;
}
public List getClaims() {
return claims;
}
// ...
}
Or using record
if you use java 14+
public record User(String name, int age, String address, List claims) {
public User {
requireNonNull(name);
requirePositive(age);
requireNonNull(address);
claims = List.copyOf(requireNonNull(claims));
}
}
The Builder pattern simulates named parameters available in Python/Scala/Kotlin. It makes client code easy to read and write, and it enables you to work with optionals / parameters with default values more fluently.
public class User {
private final UUID id;
private final Instant createdAt;
private final Instant updatedAt;
private final String firstName;
private final String lastName;
private final Email email;
private final Optional age;
private final Optional middleName;
private final Optional address;
public User(UUID id,
Instant createdAt,
Instant updatedAt,
String firstName,
String lastName,
Email email,
Optional age,
Optional middleName,
Optional address) {
this.id = requireNonNull(id);
this.createdAt = requireNonNull(createdAt);
this.updatedAt = requireNonNull(updatedAt);
this.firstName = requireNonNull(firstName);
this.lastName = requireNonNull(lastName);
this.email = requireNonNull(email);
this.age = requireNonNull(age);
this.middleName = requireNonNull(middleName);
this.address = requireNonNull(address);
if (firstName.isBlank()) {
// throw exception
}
// ... validation
}
// ...
}
// And then you would write:
public User createUser() {
final var user = new User(
randomUUID(),
now(),
now().minus(1L, DAYS),
// firstName, lastName and email are String, what if you
// mix up parameters order in constructor?
"first_name",
"last_name",
"user@test.com",
empty(),
Optional.of("middle_name"),
empty()
);
// ...
return user;
}
public class User {
private final UUID id;
private final Instant createdAt;
private final Instant updatedAt;
private final String firstName;
private final String lastName;
private final String email;
private final Optional age;
private final Optional middleName;
private final Optional address;
// private constructor
private User(Builder builder) {
this.id = requireNonNull(builder.id);
this.createdAt = requireNonNull(builder.createdAt);
this.updatedAt = requireNonNull(builder.updatedAt);
this.firstName = requireNonNull(builder.firstName);
this.lastName = requireNonNull(builder.lastName);
this.email = requireNonNull(builder.email);
this.age = requireNonNull(builder.age);
this.middleName = requireNonNull(builder.middleName);
this.address = requireNonNull(builder.address);
if (firstName.isBlank()) {
// throw exception
}
// ... validation
}
// ...
public static class Builder {
private UUID id;
private Instant createdAt;
private Instant updatedAt;
private String firstName;
private String lastName;
private String email;
// Optionals are empty by default
private Optional age = empty();
private Optional middleName = empty();
private Optional address = empty();
private Builder() {
}
public static Builder newUser() {
// You can easily add (lazy) default parameters
return new Builder()
.id(randomUUID())
.createdAt(now())
.updatedAt(now());
}
public Builder id(UUID id) {
this.id = id;
return this;
}
public Builder createdAt(Instant createdAt) {
this.createdAt = createdAt;
return this;
}
// ...
public Builder address(Address address) {
this.address = Optional.ofNullable(address);
return this;
}
public User build() {
return new User(this);
}
}
}
// And then:
public User createUser() {
// You end up writing more code in User class but the
// class API becomes more concise
final var user = newUser()
.updatedAt(now().minus(1L, DAYS))
.firstName("first_name")
.lastName("last_name")
.email("user@test.com")
.middleName("middle_name")
.build();
// ...
return user;
}