LanguagesJavaScriptJava Best Practices

Java Best Practices

In computer programming, best practices are a set of informal rules that many developers follow to improve software quality, readability, and maintainability. Best practices are especially beneficial where an application remains in use for long periods of time, so that it was initially developed by one team and then subsequently maintained by a different group of people.

The Working with Java Variables article presented a number of best practices for variable naming. This series will now go much further and cover such topics as class member scoping, try/catch blocks, and even the formatting of constant values. This tutorial will provide an overview of the best practices for Java that we will be exploring over the next several weeks, along with an explanation of each entry in our top best practices for Java programming list.

Java Programming Best Practices at a Glance

Although the complete list of Java Best Practices can be a lengthy one, there are several which are considered to be a good starting place for coders who are taking their first steps into improving their code quality, including using proper naming conventions, make class members private, avoiding the use of empty catch blocks, avoiding memory leaks, and properly commenting code blocks:

  • Use Proper Naming Conventions
  • Class Members Should be Private
  • Use Underscores in lengthy Numeric Literals
  • Avoid empty catch Blocks
  • Use StringBuilder or StringBuffer for String Concatenation
  • Avoid Redundant Initializations
  • Using enhanced for loops instead of for loops with a counter
  • Proper handling of Null Pointer Exceptions
  • Float or Double: which is the right choice?
  • Use of single quotes and double quotes
  • Avoiding Memory leaks
  • Return Empty Collections instead of returning Null elements
  • Efficient use of Strings
  • Unnecessary Objects Creation
  • Proper Commenting

Class Members in Java Should be Private

In Java, the more inaccessible the members of a class are, the better! The first step is to use the private access modifier. The goal is to foster ideal encapsulation, which is one of the fundamental concepts of object-oriented programming (OOP). All-too-often, new developers fail to properly assign access modifiers to the classes or prefer to keep them public to make things easier.

Consider this class where fields are made public:

public class BandMember {
  public String name;
  public String instrument;
} 

Class encapsulation is compromised here as anyone can change these values directly like so:

BandMember billy = new BandMember();
billy.name = "George";
billy.instrument = "drums";

Using private access modifier with class members keeps the fields hidden preventing a user from changing the data except via setter methods:

public class BandMember {
  private String name;
  private String instrument;
  
  public void setName(String name) {
    this.name = name;
  }
  public void setInstrument(String instrument)
    this.instrument = instrument;
  }
}

Setters are also the ideal place to put validation code and/or housekeeping tasks such as incrementing a counter.

You can learn more about access modifiers in our tutorial: Overview of Java Modifiers.

We also have a great tutorial covering the concept of Java Encapsulation if you need a refresher.

Use Underscores in Lengthy Numeric Literals

Thanks to a Java 7 update, developers can now write lengthy numeric literals that have increased readability by including underscores (_). Here are some lengthy numeric literals before underscores were permitted:

int minUploadSize = 05437326;
long debitBalance = 5000000000000000L;
float pi = 3.141592653589F;

I think you will agree that underscores make the values more readable:

int minUploadSize = 05_437_326;
long debitBalance = 5_000_000_000_000_000L;
float pi = 3.141_592_653_589F;

Avoid Empty Catch Blocks

It is a very bad habit to leave catch blocks empty, for two reasons: it can either cause the program to silently fail, or continue along as if nothing happened. Both of these outcomes can make debugging significantly harder.

Consider the following program which calculates sum of two numbers from command-line arguments:

public class Sum {
  public static void main(String[] args) {
    int a = 0;
    int b = 0;
    
    try {
      a = Integer.parseInt(args[0]);
      b = Integer.parseInt(args[1]);
    } catch (NumberFormatException ex) {
    }
    
    int sum = a + b;
    
    System.out.println(a + " + " + b + " = " + sum);
  }
}

Java’s parseInt() method throws a NumberFormatException, requiring the developer to surround its invocation within a try/catch block. Unfortunately, this particular developer chose to ignore thrown exceptions! As a result, passing in an invalid argument, such as “45y” will cause the associated variable to be populated with the default for its type, which is 0 for an int:

Java Sum Class Error

Generally, when catching an exception, programmers should take one or more of the following three actions:

  1. At the very least, inform the user about the exception, either get them to re-enter the invalid value or let them know that the program must abort prematurely.
  2. Log the exception using JDK Logging or Log4J.
  3. Wrap and re-throw the exception as a new, more application-specific, exception.

Here is our Sum application rewritten to inform the user that an input was invalid and that the program will be aborting as a result:

public class Sum {
  public static void main(String[] args) {
    int a = 0;
    int b = 0;
    
    try {
      a = Integer.parseInt(args[0]);
    } catch (NumberFormatException ex) {
      System.out.println(args[0] + " is not a number. Aborting...");
      return;
    }
    
    try {
      b = Integer.parseInt(args[1]);
    } catch (NumberFormatException ex) {
      System.out.println(args[1] + " is not a number. Aborting...");
      return;
    }
    
    int sum = a + b;
    
    System.out.println(a + " + " + b + " = " + sum);
  }
}

We can observe the results below:

Java error handling

Use StringBuilder or StringBuffer for String Concatenation

The “+” operator is a quick and easy way to combine Strings in Java. Before the days of Hibernate and JPA, objects were persisted manually, by building an SQL INSERT statement from the ground up! Here is one that stores some user data:

String sql = "Insert Into Users (name, age)";
       sql += " values ('" + user.getName();
       sql += "', '" + user.getage();
       sql += "')";

Unfortunately, when concatenating numerous strings as above, the the Java compiler has to create multiple intermediate String objects before amalgamating these into the final concatenated string.

Instead, we should be using either the StringBuilder or StringBuffer class. Both include functions to concatenate strings without having to creating intermediate String objects, therefore saving processing time and unnecessary memory usage.

The previous code could be rewritten using StringBuilder as follows:

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

It is a bit more work on the developer’s part, but well worth it!

StringBuffer vs StringBuilder

While both the StringBuffer and StringBuilder classes are preferable to the “+” operator, both are not created equally. The StringBuilder is the faster of the two, but is not thread-safe. So, for String manipulations in a non-multi threaded environment, we should use StringBuilder; otherwise use the StringBuffer class.

Avoid Redundant Initializations

Although certain languages like TypeScript strongly encourage you to initialize your variables at declaration time, this is not always necessary in Java, as it assigns default initialization values – like 0, false and null – to variables upon declaration.

Therefore, a java best practice is to be aware of the default initialization values of member variables while not initializing your variables explicitly unless you’d like to set them to values other than the defaults.

Here is a short program that calculates the sum of natural numbers from 1 to 1000. Notice that only some of the variables are initialized:

class VariableInitializationExample {
  public static void main(String[] args) {
    
    // automatically set to 0
    int sum; 
    final numberOfIterations = 1000;

    // Set the loop counter to 1
    for (int i = 1; i &= numberOfIterations; ++i) {
      sum += i;
    }
       
    System.out.println("Sum = " + sum);
  }
}

Using Enhanced For Loops Instead of For Loops with a Counter

Although for loops certainly have their place, there is no question that the counter variable can be a source of errors. For instance, the counter variable can incidentally get altered, before is is used later in the code. Even starting the index from 1 instead of 0 can result in unexpected behavior. For these reasons, the for-each loop (also referred to as an enhanced for loop) may be a better option.

Consider the following code:

String[] names = {"Rob", "John", "George", "Steve"}; 
for (int i = 0; i < names.length; i++) {
  System.out.println(names[i]);
}

Here, variable i is performing double duty as a counter for a loop as well as the index for the array names. Although this loop only prints out each name it could become problematic if there were code further down the line that modified i. We can easily sidestep the whole issue by using a for-each loop like the one shown below:

for (String name : names) {
  System.out.println(name);
}

Far less chance of errors there!

Proper handling of Null Pointer Exceptions

Null Pointer Exceptions are a surprisingly common occurrence in Java, likely due to its Object-Oriented design. Null Pointer Exceptions occur whenever you attempt to invoke a method on a Null Object Reference. This usually happens when you call an instance method before the class has been instanciated, such as in the case below:

Office office;

// later in the code...
Employee[] employees = office.getEmployees();

While you can never completely eliminate Null Pointer Exceptions, there are ways to mimimize them. One approach is to check for Null objects prior to invoking one of its methods. Here’s an example that uses the ternary operator:

Office office;

// later in the code...
Employee[] employees = office == null ? 0 : office.getEmployees();

You might also want to throw your own Exception:

Office office;
Employee[] employees;

// later in the code...
if (office == null) {
  throw new CustomApplicationException("Office can't be null!");
} else { 
  employees = office.getEmployees();
}

Float or Double: Which to Use?

Floats and doubles are similar types, so it’s no surprise that many developers are unsure of which to go with. Both deal with floating-point numbers, but possess very different features. For example, float has a size of 32 bits while double is allocated twice as much memory at 64 bits. Hence, double can handle much bigger fractional numbers than float. Then there’s the issue of precision: float only accommodate 7 bits of precision. Having an extremely small exponent size means some of the bits are inevitable lost. Compare that to the double, which allocates a far greater number of bits for the exponent, allowing it to handle up to 15 bits of precision.

Therefore, float is generally recommended when speed is more important than accuracy. While most programs do not involve large computations, the difference in precision can becomes quite significant in math-intensive applications. Float is also a good choice when the number of decimal digits required is known in advance. When precision is highly important, double should be your go-to. Just keep in mind that Java enforces the use of double as the default data type for dealing with floating-point numbers so you may want to append the letter “f” to explicitly denote the float, e.g., 1.2f.

Use of Single Quotes and Double Quotes in Concatenation

In Java, double quotes (“) are used to hold strings and single quotes are for characters (represented by the char type). Problems can occur when we try to concatenate characters together using the + concatenation operator. The problem is that concatenating chars using + converts the value of the char into ascii and hence yields a numerical output. Here is some example code to illustrate this point:

char a, b, c;
a = 'a';
b = 'b';
c = 'c';

str = a + b + c; // not "abc", but 294!

Just as string concatenation is best accomplished using either the StringBuilder or StringBuffer class, so too with characters! In the above code, the a, b and c variables may be combined as a string as follows:

new StringBuilder().append(a).append(b).append(c).toString()

We can observe the desired results below:

Java char Concatenation

 

Avoiding Memory Leaks in Java

In Java, developers do not have much control over memory management, as Java takes care of that automatically via the process of garbage collection. Having said that, there are some Java best practices that developers can employ to help prevent memory leaks, such as:

  • Try not to create unnecessary objects.
  • Do not use the + operator for String Concatenation.
  • Do not store a massive amount of data in the session.
  • Time out the session when no longer being used.
  • Avoid the use of static objects because they live for the entire life of the application.
  • When working with a database, don’t forget to close the ResultSet, Statements, and Connection objects in the finally block.

Return Empty Collections instead of the Null Reference

Did you know that the null reference is often referred to as the worst mistake in software development? The null reference was invented in 1965 by Tony Hoare when he was designing the first comprehensive type system for references in Object Oriented Language (OOP). Later, at a conference in 2009, Hoare apologized for inventing it, admitting that his creation “has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.”

It is a Java best practice to return empty values rather than nulls in general, but it is especially important when you return a collection, enumerable, or an object. Although your own code may handle the returned null value, other developers may forget to write a null check or even realize that null is a possible return value!

Here is some Java code that fetches a list of books in stock as an ArrayList. However, if the list is empty, an empty list is returned instead:

private final List<Book> booksInStock = ...

public List<Book> getInStockBooks() {
  return booksInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(booksInStock);
}

That allows callers of the method to iterate over the list without first having to check for a null reference:

(Book book: getInStockBooks()) {
  // do something with books
}

Efficient use of Strings in Java

We have already talked about the side effects with the use of the + concatenation operator, but there are other things we can do to utilize strings more efficiently so as to not waste memory and processor cycles. For example, when instantiating a String object, it is considered a Java best practice to create the String directly rather than use the constructor. The reason? It is way faster to create the String directly compared to using a constructor (not to mention less code!).

Here are two equivalent ways of creating a String in Java; directly vs. using a constructor:

// directly
String str = "abc";
     
// using a constructor
char data[] = {'a', 'b', 'c'};
String str = new String(data);

While both are equivalent, the direct approach is the better one.

Unnecessary Object Creation in Java

Did you know that object creation is one of the most memory consuming operations in Java? For that reason, it is best to avoid creating objects for no good reason and only do so when absolutely required.

So how would one go about putting that into practice? Ironically, creating Strings directly as we saw above is one way to avoid creating objects unnecessarily!

Here is a more complex example:

The following Person class includes an isBabyBoomer() method that tells whether the person falls into the “baby boomer” age group and was born between 1946 and 1964:

public class Person {
  private final Date birthDate;
  
  public boolean isBabyBoomer() {
    // Unnecessary allocation of expensive object!
    Calendar gmtCal =
        Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomStart = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomEnd = gmtCal.getTime();
    
    return birthDate.compareTo(boomStart) >= 0 &&
           birthDate.compareTo(boomEnd)   <  0;
  }
}

The isBabyBoomer() method unnecessarily creates a new Calendar, TimeZone, and two Date instances each time it is invoked. One way to rectify this inefficiency is to employ a static initializer so that the Calendar, TimeZone, and Date objects are created only once, upon initialization, rather than every time isBabyBoomer() is invoked:

class Person {
  private final Date birthDate;
  
  // The starting and ending dates of the baby boom.
  private static final Date BOOM_START;
  private static final Date BOOM_END;
  
  static {
    Calendar gmtCal =
      Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_START = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_END = gmtCal.getTime();
  }
  
  public boolean isBabyBoomer() {
    return birthDate.compareTo(BOOM_START) >= 0 &&
       birthDate.compareTo(BOOM_END)       <  0;
  }
}

Proper Commenting in Java

Clear and concise comments can be a real life saver when trying to decipher another developer’s code. Here are a few guidelines for writing quality comments*:

  1. Comments should not duplicate the code.
  2. Good comments do not excuse unclear code.
  3. If you can’t write a clear comment, there may be a problem with the code.
  4. Explain unidiomatic code in comments.
  5. Include links to external references where they will be most helpful.
  6. Add comments when fixing bugs.
  7. Use comments to mark incomplete implementations. (these will usually start with the tag “TODO:”)

Final Thoughts on Java Best Practices

In this installment of the Java Best Practices series, we learned about the top 15 Java best practices and explored class member encapsulation, the use of underscores in lengthy numeric literals, avoiding empty catch blocks, String concatenation done right, how to avoid redundant initializations, as well as using enhanced for loops.

Read: Top Online Java Training Courses and Bundles

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories