Java Best Practices and Tips by Toptal Developers

Share

This resource contains a collection of Java best practices and Java tips provided by our Toptal network members.

This resource contains a collection of Java best practices and tips provided by our Toptal network members. As such, this page will be updated on a regular basis to include additional information and cover emerging Java techniques. This is a community driven project, so you are encouraged to contribute as well, and we are counting on your feedback.

Java is one of the most portable languages, as with Java it is possible to build a highly distributed web application, a sophisticated desktop application, or even a powerful mobile application running on a handheld device.

Check out the Toptal resource pages for additional information on Java common mistakes, a Java hiring guide, Java job description and Java interview questions.

Always Check for Parameter Preconditions

As a part of the “fail fast” approach, public methods should always validate input parameters before executing any logic. This way, you avoid getting an error later, where the cause is harder to find.

Since version 1.4, Java has supported the assert keyword for this purpose. For example:

public void setAge(int age) {
    assert age >= 0 : "Age must be positive";
    …
}

An advantage of using assertions is that they can be disabled at runtime to avoid any performance impact in a production system.

Another approach is to use Google Guava, where you can use Preconditions class, which provides static methods. This way, you can get more accurate exceptions, as assertions throw only a generic AssertionError. The following example will throw an IllegalArgumentException:

public void setAge(int age) {
    Preconditions.checkArgument(assert age >= 0, "Age must be positive");
    …
}

In a similar way, you could use Preconditions.checkState(…) to validate post conditions, which throws an IllegalStateException. For a full list of precondition methods, check the Guava documentation.

Checking for parameter nullness is a particularly useful case for precondition validation. It is a good practice to check all the parameters that must not be null. Another good practice is to mark nullable parameters with javax.annotation.Nullable annotation. This way, your code is more readable. Adding Nullable annotation to methods that can return null values is also a good practice.

public void updateCustomer(int id, String name, @Nullable String address) {
    Preconditions.checkNotNull(name, "Name is mandatory.");
    …
}

If you are using Java 7 or beyond, you can use Objects.requireNonNull(…), which has the same functionality.

public void updateCustomer(int id, String name, @Nullable String address) {
    Objects.requireNonNull(name, "Name is mandatory.");
    …
}

Get the latest developer updates from Toptal.

Subscription implies consent to our privacy policy

Few Tips for Writing Better Documentation

Some simple recommendations to write better documentation:

  • Write Javadoc comments for each public class and method (maybe setters and getters could be omitted).
  • Avoid irrelevant and obvious comments. A comment saying “sets the name” on a setter provides no extra information.
  • Explain the class purpose and its behavior.
  • Explain the method contract and what is the expected behavior. Which input parameter values are valid? What values can be expected as output? Under which scenarios exceptions will be thrown? A good approach is to document pre and post conditions.
  • Use meaningful, self-documenting names for classes, methods, and variables.
  • Avoid unnecessary comments inside methods, especially since it’s too easy for comments and code to get out of sync with one another over time. If you write too many comments, maybe it is better to split the method into self-documenting private methods. For example, instead of adding a comment double finalPrice = price + (price * taxRate); // computes taxes, a better approach would be to create a private method, which is easier to read: double finalPrice = computeTax(price, taxRate);

How to Properly Use Utility Classes

Utility classes are classes that are not intended to be instantiated. Instead, they typically provide static methods for generic functionality, and they are like global functions in OOP. So, if you are writing too many utility classes, you should review your coding design. However, sometimes you will need to write some utility class, a good example being generic functions. Another example is internal Domain Specific Languages (DSL). When implementing a DSL in Java, you typically don’t follow OOP principles and write classes with static methods, since the goal is to make syntax shorter, rather than reusable components.

However, if you face a scenario where you prefer to write a utility class, consider the following recommendations:

  • Make the constructor private. This way, the class can’t be instantiated.
  • Make the class final. Even when a private constructor will restrict instantiation in subclasses, somebody could create a subclass and add more static methods.
  • Create utility classes that are cohesive according to the functionality provided by their methods. Generic names like MvcUtils or CommonUtils usually lead to a bad design.

Don’t Make Excessive Use of Reflection

Reflection is a cool Java feature as it allows querying for class structure and dynamically making calls to methods, object instantiation, or setting field values. This feature is widely used in frameworks and libraries to build generic code that works in different classes.

Having the ability to execute code dynamically is powerful. However, if you use too much reflection in your code, you could jeopardize the code maintainability. Consider the following simple example: You need to copy fields from one Java Bean to another that has fields with the same name.

public class PersonEntity {
	private String firstName;
	private String lastName;

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
	return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
}

public class PersonDTO {
	private String firstName;
	private String lastName;

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
}

A viable option here is to iterate over getters and copy the values using reflection. For the sake of simplicity, I’m assuming that getters in the source object return exactly the same type as a setter in the target. Such an implementation could look like this:

public void copyWithReflection(Object source, Object target) throws 
		IllegalAccessException,
		IllegalArgumentException,
		InvocationTargetException,
		NoSuchMethodException,
		SecurityException {
	requireNonNull(source, "source can't be null");
	requireNonNull(target, "target can't be null");

	for (Method getter : source.getClass().getMethods()) {
		if (getter.getName().startsWith(GETTER_PREFIX)) {
			try {
			Method setter = target.getClass()
					.getMethod(SETTER_PREFIX + getter.getName()
					.substring(GETTER_PREFIX.length()),
							getter.getReturnType());
			setter.invoke(target, getter.invoke(source));
			} catch (NoSuchMethodException e) {
				// Nothing to do, setter does not exist.
			}
		}
	}
}

At first sight, this seems like a good idea. You can use the same logic for copying values in different classes. But what happens if, in the future, you need to copy only some fields? A viable option here is to add annotations to getters, but that would add unnecessary complexity. Even worse, what happens if in the future you need some type of data conversion? What happens if you change a field in the source, but you forget to add it to the target?

Probably a better and simpler approach would be just copying properties explicitly:

public void copyWithoutReflection(PersonEntity source, PersonDTO target) {
	target.setFirstName(source.getFirstName());
	target.setLastName(source.getLastName());
}

This approach has the advantage of being simpler, more readable, deals with fewer exceptions and maintains compile-time checking for method names. And it is more flexible since getters and setters don’t need to have the same name.

First generation dependency injectors, such as Spring, used XML-based configuration plus reflection to set-up components, thus losing compile-time type checking. Later, more advanced injectors such as Guice and Dagger added Java-based configuration, to add more compile-time checking, but the reflection was still used. Dagger 2 utilizes a 100% code generation based approach, eliminating the need for reflection and providing compile time validations for injector set-up.

Avoid String Concatenation in a Large Loop

When you add two string in a loop (for, while, do-while), concatenating them using the + operator causes wastage of memory and increases performance time. This is because a new String Object is created each time a new String is added. Instead, the best practice is to use a StringBuilder class.

Let’s show it with the examples:

//Bad Practice
String output = "";
for (String s : largeArray) {
    output = output  + s;
}

If we add this Java snippet to the bytecode and then the generated bytecode decompile back, we will get the following code:

String output = "";
for( String s : largeArray ) { 
    output = new StringBuilder().append(output).append(s).toString(); 
} 
return output;

As we can see, Java used an internal StringBuilder, and in the loop, two operations are executed each time.

So, a much better solution would be to use a StringBuilder in the first place:

//Best Practice
StringBuilder sb = new StringBuilder();
for (String s : largeArray) {
    sb.append(s);
}
String output = sb.toString();

To confirm this, I tested both scenarios (i.e., using the ‘+’ operator plus the StringBuilder). Here are the results:

Loop lengthUsing String '+' Using String Builder Append
 Trial 1Trial 2Trial 3Trial 4Average (ms) Trial 1Trial 2Trial 3Trial 4Average (ms)
10003248426246 4553344043
5000105128122113117 2952433640
10000222262262278256 3672455552
3000016891713174716861708.75 4353545350.75
5000044544445434543574400.25 4263656358.25
7000065676933694270746879 5172616963.25
900001026310688106981051610541.25 5255726160


StringBuilder graph

Testing PC:

  • Intel Core i5 M 540 @ 2.53GHz
  • 8GB RAM
  • Windows 10 Pro
  • Java 1.8

As we can see, the results confirm the original assertion.

To learn more, Laurent gave a detailed explanation of the process using the disassembled bytecode in Java 8.

Contributors

Akinola Olayinka

Java/Android Developer (Shopping Internet Services)

Akinola is a Java and Android enthusiast, and a freelance developer with over three years of experience.

Use Generics to Enforce Compile-Time Type Checking

Java, as a strongly typed language, provides a generic type system. Designing how your classes will make use of generic types could be hard and tedious, but it offers significant advantages. The resulting code is cleaner since you will no longer need most cast operations. Also, the code will be safer, since more type validations will be done at compile time, thus avoiding ClassCastException errors at runtime.

But, as said before, generic types could be complicated. As generics is a big topic, reading additional Java documentation is a good idea.

Dealing with the Invariant Nature of Generics

Let’s start explaining the meaning of variance. In object-oriented (OO) programming, it is natural to think of superclasses as more generic types than subclasses. We can think of superclasses as “bigger” types since they could include more classes than specific subclasses. For example, java.lang.Number, could be considered as a bigger type than java.lang.Integer, since it can also contain java.lang.Double, java.lang.Float, and so on.

While this is quite simple in the standard OO inheritance, it could become a little harder when dealing with more complex type systems, such as the one defined by Java generics. Specific concepts needs to be defined to specify which type is bigger than the other.

According to Wikipedia, four categories can be used: * Type A is covariant to B if A >= B. This mean, a variable of type A can hold instances of B. * Type A is contravariant to B if A <= B. This is almost the opposite to previous point. * A type is bivariant if it is both covariant and contravariant at the same time. This is not supported by Java. * A type is invariant if it does not fit on any of the previous definitions.

In Java, generics are invariant by default (in contrast to arrays, which are covariant). This kind of incoherence can lead to confusion. For example, this code is valid in Java:

String stringArray[] = { "Hi", "how", "is", "it", "going?" };
Object objectArray[] = stringArray;

To explain this a bit: String[] is a subtype of the Object[]. The problem with this approach is that it could lead to runtime exceptions. For example, executing the following line will produce an java.lang.ArrayStoreException exception:

objectArray[0] = 123; 

To avoid this, generics types are not covariant by default and, this way, the compiler can prevent these kinds of errors. The following code will produce a compile-time error on the second line:

List stringList = new ArrayList<>();
List objectList = stringList;


But what happens if we need to use a more generic or more specific type parameter? Here is where extends and super keywords come into play.

With extends you can indicate that a type parameter can be any class extending the specified one. For example:

List integerList = new ArrayList<>();
List numberList = integerList;

This code block is valid. Will this have the same problem as arrays? No, because the Java compiler can check type parameters at compile time. So, while integerList.add(Integer.valueOf(123)); is valid, numberList.add(Integer.valueOf(123)); will produce a compilation error. However, reading an element and asking for a method from a Number class is perfectly valid:

numberList.get(0).intValue();

This is valid because, even when the compiler does not know exactly which class is parameterized, it knows that it must be a Number subclass. So, any method found in this class can be called.

In a similar way, the super keyword indicates that the class parameter could be any superclass of the specified one.

Dealing With Raw Types and Type Erasure

Another topic that is worth taking into account is raw types and type erasure. When generics were introduced with Java 1.5, Java implemented a workaround called “type erasure” to maintain backward compatibility with older versions of Java. This means that information about generics are removed by the compiler from variable types, method parameter types, and method return types. The compiler also automatically adds cast operations when needed. You can find out more about how type erasure works in the online Java tutorial

Type erasure enables the use of raw types. Raw types are used when declaring a variable. With this kind of declaration, you can just omit the generic type declaration. For example, the following code compiles (with warnings):

List integerList = new ArrayList<>();
List rawList = integerList;

Note that rawList lacks a generic type definition because it is a raw type. The use of raw types is discouraged because it could lead to problems similar to the ones that could happen when working with arrays. The following line of code is valid but will produce a java.lang.ClassCastException exception.

rawList.add("Hi");

Most of the time you will not notice type erasure, but sometimes you will need to be aware of it. For example, some frameworks, like Guice, force you to use subclassing to keep generic type information at runtime, to do generic-type bindings. In Guice, if you need to specify a binding using a generic type, you need to extend the TypeLiteral class:

bind(new TypeLiteral>(){})
    .to(SomeImplementation.class);

The reason to do this is type erasure. As explained before, Java removes generic information from method parameter types, but it is kept when subclassing, and thus is available through reflection.

Note that .NET designers took a different approach when implementing generics on such a platform. There is no type erasure there. For example, Ninject (a .NET dependency injector) allows setting bindings like this:

this.Bind().To();

This is because information about type parameter IWeapon is maintained at runtime when making the call to the Bind method.

Avoid Object Mutability

In Java, an object can be either mutable or immutable. Being mutable means that object state and variable values can be changed during its lifecycle, while immutability means the opposite; the state can’t be changed once the object is created. It is important to remember object mutability could be a problem; it might become hard to track where and when the object state changed since many variables can point to the same instance. Because of this, you should avoid mutability when possible.

A good practice (even when it doesn’t guarantee immutability) is to make all the fields final:

public class Person {
  private final int id;
  private final String name;

  public Person(int id, String name) {
    this.id = id;
    this.name = Preconditions.checkNotNull(name);
  }
  …
}

This way, the Java compiler forces you to set a value in the constructor for each final field. Even if you have more than one constructor, the compiler will track all the paths to ensure that final fields will be initialized.

Dependency injectors, like Spring or Guice, allows you to implement different injection models, by using setter, field, constructor. To keep object immutability, it is better to use constructor-based dependency injector.

Learn to Use Utility Classes to Properly Implement `equals()` and `hashCode()` Methods

If you are going to use the different collection classes provided by Java, you should be familiar with the requirements for implementing equals() and hashCode() methods. Alternatively, you could read more about the contract specification at the Object class javadoc. Implementing these methods could be tedious. Most IDEs provide code generation, but generated methods could be verbose. Typically, when you change the class structure, you will need to regenerate these methods, and you will lose any changes you could have made manually. Also, it could be error-prone as you will need to select all the involved fields again. Here’s an example of Eclipse-generated methods:

@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + ((key == null) ? 0 : key.hashCode());
	result = prime * result + ((value == null) ? 0 : value.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;
	MyClass other = (MyClass) obj;
	if (key == null) {
		if (other.key != null)
			return false;
	} else if (!key.equals(other.key))
		return false;
	if (value == null) {
		if (other.value != null)
			return false;
	} else if (!value.equals(other.value))
		return false;
	return true;
}

Luckily, Java 7 and Google Guava provide utility classes for making such tasks easier. Java 7 provides two static methods: Objects.hash() for building hash codes and Objects.equals() for comparing objects for equality.

For example, you could do something like this:

@Override
public int hashCode() {
  return Objects.hash(key, value);
}

@Override
public boolean equals(Object o) {
  if (this == o) {
    return true;
  }
  if (o == null || getClass() != o.getClass()) {
    return false;
  }
  MyClass that = (MyClass) o;
  return Objects.equals(key, that.key)
      && Objects.equals(value, that.value);
}
  • Objects.hash() computes a hash code from the hash codes of the objects provided as arguments.
  • Objects.equals() compares two objects, making all the nullness validations to avoid NullPointerExceptions.

As stated, if you are using a pre-Java 8 version, you could use Guava’s com.google.common.base.Objects class, which provides similar methods.

Submit a tip

Submitted questions and answers are subject to review and editing, and may or may not be selected for posting, at the sole discretion of Toptal, LLC.

* All fields are required

Toptal Connects the Top 3% of Freelance Talent All Over The World.

Join the Toptal community.