Skip to content

Java Best Practices

This document consists of the best practices related to Java programming for the course.

Use Proper Naming Conventions

First thing first, before start writing code, set a proper naming convention for your Java project (refer to our coding standards). Pre-decide names for every class, interfaces, methods and variables etc. If other developers are also working with you on the same project, they should also follow it to maintain the uniformity. A meaningful naming convention is extremely important as everything, from classes to interfaces are identified from their names in the code.

Do not assign random names just to satisfy the compiler, use meaningful and self-explanatory names so that it is readable and can be later understood by yourself, your teammates, quality assurance engineers and by staff who will be handling maintenance of the project.

Avoid Redundant Initializations

Although it is very common practice, it is not encouraged to initialize instance variables with the values: like 0, false and null. These values are already the default initialization values of instance variables in Java. Therefore, a java best practice is to be aware of the default initialization values of instance variables and avoid initializing the variables explicitly.

Unnecessary Object Creation

Object creation is one of the most memory consuming operation. That is why the best java practice is to avoid making any unnecessary objects and they should only be created when required.

Proper Commenting

Use comments to give an overview of code and provide additional information that is not readily available in the code itself. Moreover, comments should contain only information that is relevant to reading and understanding the program.

Java programs can have two kinds of comments: implementation comments and documentation comments. Implementation comments are delimited by /*…*/, and //. Documentation comments (known as “doc comments”) are Java-only, and are delimited by /**…*/. Doc comments can be extracted to HTML files using the javadoc tool.

Implementation comments are for commenting out code or for comments about the particular implementation. Doc comments are to describe the specification of the code, from an implementation-free perspective, to be read by developers who might not necessarily have the source code at hand. The frequency of comments sometimes reflects poor quality of code. When you feel compulsory to add a comment, consider rewriting the code to make it clearer.

  • Don’t assume your reader has context: Let your readers know the context behind the code so they can understand why each part is necessary. If you’ve modified code to fix a bug, comments help keep that bug fixed.
  • Explain the “Why”: Don’t tell us what we can already see in the code. Explain the why behind it. We can see which method or function you’re using, but knowing why helps readers better understand the code.

Referencing Other Code

Referencing another person's code in your own code is a common practice in software development, especially when you are using libraries, frameworks, or open-source code. Properly referencing and giving credit to the original source is important for ethical and legal reasons. Here's how you can reference someone else's code in your own code:

  • Include Comments: Add comments in your code to indicate where you are using someone else's code. Describe the purpose of the code and provide a reference to the original source.

  • Provide URLs: Include URLs or links to the original source of the code, whether it's a GitHub repository, a website, or a specific forum post. This allows others to find the original code for reference.

  • Mention the Author's Name: Mention the name of the author or contributor whose code you are using. This gives proper credit to the individual or team responsible for the code.

  • Check for Code Quality and Security: When using external code, be sure to review it for quality and security. Don't blindly include code without understanding its implications on your project.

// This section of code is adapted from Kari Brown's example on Stack Overflow:
// https://stackoverflow.com/a/1234567
  • Follow License Terms: If the code you are using is subject to a specific open-source license, make sure to follow the terms of that license. Some licenses require you to include the full license text or specify any modifications you've made.

  • Include Copyright Notices: If the code you are using has a specific license or copyright notice, make sure to include it in your code. This is especially important for open-source projects.

/*
 * Copyright (c) 20XX, Kari Brown
 * Licensed under the MIT License.
 * See https://opensource.org/licenses/MIT for details.
 */

Efficient use of Strings

String handling is very easy in Java but it must be used efficiently to prevent access memory usage. For instance, if two Strings are concatenated in a loop, a new String object will be created at every iteration. If the number of loops is significant, it can cause a lot of wasteful memory usage and will increase the performance time as well. Another case would be of instantiating a String Object, a java best practice is to avoid using constructors for instantiation and instantiation should be done directly. It is way faster as compared to using a constructor.

Use StringBuilder or StringBuffer for String Concatenation

Using the + operator to join Strings together is a common practice in many programming languages including Java.

This is a common practice and not a wrong, however, if you need to concatenate numerous strings, the + operator proves to be inefficient as the Java compiler creates multiple intermediate String objects before creating the final concatenated string.

The java best practice, in that case, would be using “StringBuilder” or “StringBuffer”. These built-in classes modify a String without creating intermediate String objects saving processing time and unnecessary memory usage.

For instance,

String sql = "Insert Into Users (name, age)";
sql += " values ('" + user.getName();
sql += "', '" + user.getage();
sql += "')";
The above-mentioned code could be written using StringBuilder like this,

StringBuilder sqlSb = new StringBuilder("Insert Into Users (name, age)");
sqlSb.append(" values ('").append(user.getName());
sqlSb.append("', '").append(user.getage());
sqlSb.append("')");
String sqlSb = sqlSb.toString();

Return Empty Collections Instead of Returning Null Elements

Null elements handling needs some extra work that is why If a method is returning a collection which does not have any value, a best java practice is to return an empty collection instead of Null elements. It skips the null element saving the efforts needed for testing on Null Elements.

Using Interface References to Collections

When declaring collection objects, references to the objects should be as generic as possible. This is to maximize the flexibility and protect the code from possible changes in the underlying collection implementations class. That means we should declare collection objects using their interfaces List, Set, Map, Queue and Deque. For example, the following class shows a bad usage of collection references:

public class CollectionsRef {

    private HashSet<Integer> numbers;

    public ArrayList<String> getList() {

        return new ArrayList<String>();
    }

    public void setNumbers(HashSet<Integer> numbers) {
        this.numbers = numbers;
    }
}

Look at the reference types which are collection implementation classes - this locks the code to work with only these classes HashSet and ArrayList. What if we want the method getList() to return a LinkedList and the method setNumbers() can accept a TreeSet?

The above class can be improved by replace the class references to interface references like this:

public class CollectionsRef {

    private Set<Integer> numbers;

    public List<String> getList() {
        // can return any kind of List
        return new ArrayList<String>();
    }

    public void setNumbers(Set<Integer> numbers) {
        // can accept any kind of Set
        this.numbers = numbers;
    }
}

Use Enhanced For Loops Instead of For Loops With Counter

“For” loop is used with a counter variable but a unique java best practice suggested by every top java developer is using the enhanced for loop instead of the old simple For loop. Generally, it won’t make any difference to use either of them but in some cases, the counter variable used could be very error-prone. The counter variable can incidentally get altered, it may get used later in the code or you may start the index from 1 instead of 0 which will result in disturbing the code at multiple points. To eliminate this, enhanced for loop is a good option.

Consider the following code snippet:

String[] names = {"Sam", "Mike", "John"}; 
for (int i = 0; i < names.length; i++) {
    method1(names[i]);
}

Here variable i is used as a counter for a loop as well as the index for the array names. It can get problematic later in the code. We can avoid the potential problems by using an enhanced for loop like shown below:

for (String Name1 : names) {
    Method1(Name1);
}

Use Parameterized Types

List listOfNumbers = new ArrayList();
listOfNumbers.add(20);
listOfNumbers.add("Forty");
listOfNumbers.forEach(n -> System.out.println((int) n * 2));

For instance, in the above lines of codes, we defined a list of numbers as a raw “ArrayList". Any object can be added to it since the type of the "ArrayList" wasn't specified with the type parameter.

If you check the last line in the above code, you will see that we cast elements to int, double it, and print the doubled number to standard output. The truth is that there won’t be errors in it during compilation time but during runtime the reason because we tried to cast a string to an integer.

How can this be corrected?

The only way out is by defining the collection by specifying the type of data getting stored in the collection. The lines of codes below show that:

List<Integer> listOfNumbers = new ArrayList<>();
listOfNumbers.add(20);
listOfNumbers.add("Forty");
listOfNumbers.forEach(n -> System.out.println((int) n * 2));

Nevertheless, the above-corrected version won't compile well because we tried to add a string into a collection that is expected to store integers only. Where we tried to add the string “Forty”, the compiler will show and point at an error.

Above all, parameterizing generic types is the best way out of this. By this, the compiler can see and make all the possible types hence reducing the chances of runtime exceptions.

Never Leave Catch Blocks Empty

It is a java best practice preferred by elite developers to write a proper and meaningful message in the catch block while exception handling. New developers often leave the catch block empty as initially they are the only ones working on a code but when the exception is caught by the empty catch block, when the program gets an exception, it does not show anything, making debugging harder and time-consuming.

What should you do in the catch blocks?

It’s up to you to write anything inside the catch blocks. Remember the main purpose of the catch blocks is to recover the program from the exceptions and continue execution, such as notifying the user about the error, ask he or she to wait, try again or exit, etc. Typically, you can do the following things in the catch blocks (not limited to):

Print out the exception details via System class’ methods, then exit the method:

System.out.println(exception);
System.out.println(exception.getMessage());

Print the full stack trace of the exception and exit the method:

exception.printStackTrace();
  • Log the exceptions then exit the method. You can use Java’s built-in logging functionalities or a third-part library like log4j.
  • Display an error message dialog in case the program is a desktop application (Swing or JavaFX).
  • Redirect the user to an error page in case the program is a web application (Servlets & JSP).

Catching Multiple Exceptions

There are as many catch blocks as the number of exceptions which can be thrown from the code safeguarded by the try block. Here’s an example:

try {
    LineNumberReader lineReader = new LineNumberReader(new FileReader("hello.txt"));
    String line = lineReader.readLine();
    lineReader.close();

    System.out.println(line);

} catch (FileNotFoundException exception) {
    System.out.println("Find not found");
} catch (IOException exception) {
    System.out.println("Error reading file");
} catch (Exception exception) {
    System.out.println("Error:");
}

In the above code, the first line in the tryblock can throw FileNotFoundException if the specified file could not be located on disk; and the next two lines can throw IOException if an error occurred during the reading and closing the file. Hence there are two catch blocks for handling both exceptions.

We could catch all these exception by only one catch block catch (Exception exception) {:

It’s easy to understand because Exception is the supertype of all exceptions. However, this practice is not recommended, as it makes the programmers lazy: catching one is obviously quicker than catching many. That means the programmers do not take responsibility to handle exceptions carefully. The best practice recommends catching specific exceptions so the program can handle different situations well. Java doesn’t prohibit you from catching one for all, but when doing so, you should have very good reasons to do that.

Close Resources using Try-With-Resources

Every time a program opens a resource, it should be an important practice to release them after use. Similarly, we should have a special care if any exception were to be thrown during operations on such resources. Java 7 introduced a special statement try-with-resource that we can use proactively to manage it. We can use this statement on any object that implements the AutoClosable interface. It ensures that each resource is closed by the end of the statement.

Comparing Objects

Many developers incorrectly use the operator == and equals() method. The == operator compares if object references or equal or not. The equals() method compares the content of Objects. In most of the cases, we use the equals() method to compare two objects. Use extra care in comparing strings as we may get unexpected results if we don’t know the differences between them.

References:

https://adevait.com/java/5-best-and-worst-practices-in-java-coding https://xperti.io/blogs/java-coding-best-practices/ https://javatechonline.com/java-coding-best-practices-and-standards/