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:
Generally, when catching an exception, programmers should take one or more of the following three actions:
- 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.
- Log the exception using JDK Logging or Log4J.
- 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:
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.
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.