Java Best Practices for Developers
Last Updated on
Aug 22, 2023
Key Takeaways
- Following a proper set of Java Best Practices enables the entire team to manage the Java project efficiently.
- Creating proper project & source files structures, using proper naming conventions, avoiding unnecessary objects and hardcoding, commenting the code in a correct way helps in maintaining the Project Code effectively.
- By using appropriate inbuilt methods and functionality, one can easily improve the performance of Java applications.
- For exception handling, developers must utilise the catch and finally block as and when required.
- By writing meaningful logs, developers can quickly identify and solve the errors.
For any developer, coding is a key task and making mistakes in it is quite possible. Sometimes, the compiler will catch the developer’s mistake and will give a warning but if it is unable to catch it, running the program efficiently will be difficult. And because of this, for any Java app development company, it is essential to make its team follow some of the best practices while developing any Java project. In this blog, we will go through various different types of Java best practices that will enable Java developers to create the application in a standardised manner.
1. Java Clean Coding Best Practices
Here we will have a look at the best practices of Java clean coding –
1.1 Create a Proper Project Structure
Creating the proper project structure is the first step to follow for Java clean coding best practices. Here, the developers need to divide and separate the entire code into related groups and files which enables them to identify the file objectives and avoid rewriting the same code or functions multiple times.
A good example of the Java Project structure is as follow:
- Source
- Main
- Java
- Resource
- Test
- Java
- Resource
- Main
Now, let’s understand each directory in the structure:
Directory | Purpose |
---|---|
Main |
|
Test |
|
Source files refers to the files like Controller, Service, Model, Entity, DTO, and Repositories files, while test source files refers to the test cases files which are written to test the code.
1.2 Use Proper Naming Conventions
Naming conventions mean the names of the interfaces and classes and see how to keep the names of constants, variables, & methods. The conventions set at this stage must be obeyed by all the developers in your team. Some of the best practices that entire team can follow are as follow:
- Meaningful distinctions: This means that the names given to the variables or other identifiers must be unique and they should have a specific meaning to it. For instance, giving names like i, j, k or p, q, r isn’t meaningful.
- Self-explanatory: The naming convention must be such that the name of any variable reveals its intention so that it becomes easy for the entire Java development team to understand it. For instance, the name must be like “dayToExpire” instead of “dte”. This means that the name must be self-explanatory and must not require any comment to describe itself.
- Pronounceable: The names given by the developers must be pronounceable naturally just like any other language. For instance, we can keep “generationTimestamp” instead of “genStamp”.
Besides this, there are some other general rules that are required when it comes to naming conventions and they are –
- Methods of the Java code should have names that are starting with lowercase and are verbs. For instance, execute, stop, start, etc.
- Names of class and interface are nouns which means that they must start with an uppercase letter. For instance, Car, Student, Painter, etc.
- Constant names must be in uppercase only. For instance, MIN_WIDTH, MAX_SIZE, etc.
- Underscore must be used when the numeric value is lengthy in Java code. For instance, the new way to write lengthy numbers is int num = 58_356_823; instead of int num = 58356823;.
- In addition to this, the use of camelCase notation is also done in Java programming naming conventions. For instance, runAnalysis, StudentManager, and more.
1.3 Avoid Creating Unnecessary Objects
Another best practice for Java clean coding is to avoid creating unnecessary objects. It is known as one of the best memory-consuming operations in Java. This means that the developers must only create objects that are required.
You can often avoid creating unnecessary objects by using static factory methods in preference to constructors on immutable classes.
For example, the static factory method Boolean.valueOf(String) is always preferable to the constructor Boolean(String).
The constructor creates a new object each time whenever it’s called, while the static factory method is not required to do so.
1.4 Create Proper Source File Structure
A source file is something that holds information about various elements. And when the Java compiler enforces any type of structure, a large part of it is fluid. But when some specific order is implemented in a source file it can help in improving the code readability. And for this, there are some different types of style guides available for inspiration for developers. Here is the element’s ordering style that can be used in a source file –
- Package Statement
- Import Statements
- Static and non-static imports
- One top-level Class
- Constructors
- Class variables
- Instance variables
- Methods
Besides this, the developers can also group the methods as per the scope and functionalities of the application that needs to be developed. Here is a practical example of it –
# /src/main/java/com/baeldung/application/entity/Customer.java package com.baeldung.application.entity; import java.util.Date; public class Patient { private String patientName; private Date admissionDate; public Patient(String patientName) { this.patientName = patientName; this.admissionDate = new Date(); } public String getPatientName() { return this.patientName; } public Date getAdmissionDate() { return this.admissionDate; } } |
1.5 Comment on the Code Properly
Commenting on the written code is very beneficial when other team members are going through it as it enables them to understand the non-trivial aspects. And in this case, proper care must be taken as specific and to-the-point things must be described in the comments as if not done in a required manner, the comments can confuse developers.
Besides this, when it comes to commenting on the Java code, there are two types of comments that can be used.
Comment Type | Description |
---|---|
Documentation/JavaDoc Comments |
|
Implementation/Block Comments |
|
Here, we will have a look at the code that specifies the usage of the meaningful documentation comment:
/** * This method is intended to add a new address for the employee. * However do note that it only allows a single address per zip * code. Hence, this will override any previous address with the * same postal code. * * @param address an address to be added for an existing employee */ /* * This method makes use of the custom implementation of equals * method to avoid duplication of an address with the same zip code. */ public addEmployeeAddress(Address address) { } |
Implementation Comments:
class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); // This will print “Hello, World!” } } |
1.6 Avoid Too Many Parameters in Any Method
When it comes to coding in Java, one of the best practices is to optimize the number of parameters in a method. This means that when any program has too many parameters in a method it can make interpreting the code difficult and complex.
Let’s have a look at the example of this scenario. Here is the code where there are too many parameters –
private void employeeInformation(String empName, String designation, String departmentName, double salary, Long empId) |
Here is the code with an optimized number of parameters –
private void employeeInformation(String empName, Info employeeInfo) |
1.7 Use Single Quotes and Double Quotes Properly
In Java programming, single quotes are used to specify characters in some unique cases, and double quotes for strings.
Let’s understand this with an example:
Here, to concatenate characters in Java, in order to make a string, double quotes are used as they treat characters as simple strings. Besides this, single quotes specify integer values of the characters. Let’s have a look at the below code as an example –
public class demoExample { public static void main(String args[]) { System.out.printIn("A" + "B"); System.out.printIn('C' + 'D'); } } |
Output:-
AB
135
1.8 Write Code Properly
Code written for any type of application in Java must be easier to read and understand. The reason behind it is that when it comes to Java, there is no single convention for code. Therefore, it is necessary to define a private convention or adopt a popular one. Here are the reasons why it is important and why it is essential to have indentation criteria –
- The Java developers must use four spaces for a unit of indentation.
- There must be a cap over the line length, but it can also be set to more than 80 owing.
- Besides this, expressions must be broken down with commas.
Here is the best example of it –
List<String> employeeIds = employee.stream() .map(employee -> employee.getEmployeeId()) .collect(Collectors.toCollection(ArrayList::new)); |
1.9 Avoid Hardcoding
Avoiding hard coding is another best practice that must be followed by developers. For instance, hard coding can lead to duplication and can make it difficult for the developers to change the code when required.
Besides this, it can also lead to undesirable behaviour when the values in the code are dynamic. Also, hardcoding factors can be refactored in the following manner –
- Developers must replace enum or constant value with defined methods or variables in Java.
- They can also replace class-level defined constants or the values picked from the configuration.
For Example:
private int storeClosureDay = 7; // This can be refactored to use a constant from Java private int storeClosureDay = DayOfWeek.SUNDAY.getValue() |
1.10 Review and Remove Duplicate Code
Reviewing the duplicate code is another important best practice that needs to be followed. It can happen that sometimes two or multiple methods have the same intent and functionality in your Java project code. In such cases, it becomes essential for the developers to remove the duplicate methods / class and use the single method / class wherever required.
2. Java Programming Best Practices
Here are some of the best practices of Java coding that developers can take into consideration –
2.1 Keep Class Members as Private
Class members should be private wherever possible. The reason behind it is that the more inaccessible the member variables are, the better the programming is done. This is the reason why Java developers must use a private access modifier. Here is an example that shows what happens when fields of the class are made public –
public class Student { public String name; public String course; } |
Anyone who has access to the code can change the value of the class Student as shown in the below code –
Student student= new Student(); student.name = "George"; student.course = "Maths"; |
This is why one should use private access modifiers when it comes to defining the class members. The private class members have the tendency to keep the fields hidden and this helps in preventing any user of the code from changing the data without using setter methods. For Example:
public class Student { private String name; private String course; public void setName(String name) { this.name = name; } public void setCourse(String course) this.course = course; } } |
Besides this, the setter methods are the best choice when it comes to code validation or housekeeping tasks.
2.2 For String Concatenation, Use StringBuilder or StringBuffer
Another Java best practice is to use StringBuffer or StringBuilder for String concatenation. Since String Object is immutable in Java, whenever we do String manipulation like concatenation, substring, etc., it generates a new string and discards the older string for garbage collection. These are heavy operations and generate a lot of garbage in the heap. So Java has provided StringBuffer and StringBuilder classes that should be used for String manipulation. These are mutable objects in Java and also provide append(), insert(), delete, and substring() methods for String manipulation.
Here is an example of the code where the “+” operator is used –
String sql = "Insert Into Person (name, age)"; sql += " values ('" + person.getName(); sql += "', '" + person.getage(); sql += "')"; // "+" operator is inefficient as JAVA compiler creates multiple intermediate String objects before creating the final required string. |
Now, let’s have a look at the example where the Java developer can use StringBuilder and make the code more efficient without creating intermediate String objects which can eventually help in saving processing time –
StringBuilder sqlSb = new StringBuilder("Insert Into Person (name, age)"); sqlSb.append(" values ('").append(person.getName()); sqlSb.append("', '").append(person.getage()); sqlSb.append("')"); String sqlSb = sqlSb.toString(); |
2.3 Use Enums Instead of Interface
Using enums is a good practice rather than creating an interface that is solely used to declare some constants without any methods. Interfaces are designed to define common behaviours and enums to define common values, So for defining values, usage of enums is best practice.
In the below code, you will see what creating an interface looks like –
public interface Colour { public static final int RED = 0xff0000; public static final int WHITE = 0xffffff; public static final int BLACK = 0x000000; } |
The main purpose of an interface is to carry out polymorphism and inheritance, not work for static stuff. Therefore, the best practice is to start using enum instead. Here is the example that shows the usage of an enum instead of an interface –
public enum Colour { RED, WHITE, BLACK } |
In case – the colour code does matter, we can update the enum like this:
public enum Colour { RED(0xff0000); BLACK(0x000000), WHITE(0xffffff), private int code; Color(int code) { this.code = code; } public int getCode() { return this.code; } } |
As we can see that the code is a bit complex because of the project and in such cases we can create a class that is dedicated to defining constants. An example of this is given below –
public class AppConstants { public static final String TITLE = "Application Name"; public static final int THREAD_POOL_SIZE = 10; public static final int VERSION_MAJOR = 8; public static final int VERSION_MINOR = 2; public static final int MAX_DB_CONNECTIONS = 400; public static final String INFO_DIALOG_TITLE = "Information"; public static final String ERROR_DIALOG_TITLE = "Error"; public static final String WARNING_DIALOG_TITLE = "Warning"; } |
By having a look at the code above, we can say that it is an unsaid rule that using enums or dedicated classes is a better idea than using interfaces.
2.4 Avoid Using Loops with Indexes
Developers must avoid using a loop with an index variable wherever possible. Instead they can replace it with forEach or enhanced for loop.
The main reason behind this is that the index variable is error-prone, which means that it may incidentally alter the loop’s body, or start the index from 1 instead of starting it from 0. Here is an example that enables the developer to iterate over an array of Strings:
String[] fruits = {"Apple", "Banana", "Orange", "Strawberry", "Papaya", "Mango"}; for (int i = 0; i < fruits.length; i++) { doSomething(fruits[i]); } |
2.5 Use Array Instead of Vector.elementAt()
Vector is known as a legacy implementation approach that is used with Java bundle. It is just like ArrayList but unlike it, Vector can be synchronised. It doesn’t require additional synchronisation when multiple threads try to access it but at the same time, it also degrades the performance of the Java application. And when it comes to any application, performance is the most important factor. Therefore, the array must be used instead of a Vector.
Let’s take an example where we have used the vector.elementAt() method to access all the elements.
for (String fruit : fruits) { doSomething(fruit); } |
For best practice, we can convert the vector first into an array.
int size = v.size(); for(int i=size; i>0; i--) { String str = v.elementAt(size-i); } |
2.6 Avoid Memory Leaks
Unlike most other programming languages for software development, when developers are working with Java, they do not need to have much control over memory management. The reason behind it is that Java is a programming language that manages memory automatically.
In spite of this, there are some Java best practices that experts use to prevent memory leaks because any kind of memory loss can degrade an application's performance and also affect the business.
There are few more points to prevent memory leaks in Java.
- Do not create unnecessary objects.
- Avoid String Concatenation and use String Builder and String Buffer.
- Don't store a massive amount of data in the session and time out the session when no longer used.
- Do not use the System.gc() and also avoid using static objects.
- Always close the connections, statements and result sets in the Finally block.
2.7 Debug Java Programs Properly
Debugging Java programs properly is another practice that developers need to follow. For this, there is nothing much that they need to do. The developers just have to right-click from the package explorers. Then they can select the option Debug As and choose the Java application they prefer to debug. This can help them to create a Debug Launch Configuration which can be utilized by the experts to start the Java application.
Besides this, nowadays, Java developers can edit and save the project code while they are debugging it without any need of restarting the entire program and this is possible because of the HCR (Hot Code Replacement). HCR is a standard Java approach that is added to enable expert Java developers to experiment with the code and have an iterative trial-and-error coding.
Debugging allows you to run a program interactively while watching the source code and the variables during the execution. A breakpoint in the source code specifies where the execution of the program should stop during debugging. Once the program is stopped you can investigate variables, change their content, etc. To stop the execution, if a field is read or modified, you can specify watchpoints.
Here is a post that describes the unique Java debugging tools:
2.8 Avoid Multiple if-else Statements
Another Java programming best practice is to avoid the usage of multiple if-else statements. The reason behind it is that when conditions like if-else statements are overused, they will affect the performance of the application as JVM will have to compare the conditions every now and then.
Besides this, using conditions more than required can become worse if the same one is utilized by the developers in looping statements like while, for, and more. Basically, when there are too many statements or conditions used, the business logic of the application will try to group all the conditions and offer the boolean outcome. Here is an example that shows what happens when the if-else statement is overused and why it should be avoided -
int size = v.size(); String arr[] = (String[]) v.toArray(); |
Note: The above-defined code must be avoided and the developers must use this as follows:
if (condition1) { if (condition2) { if (condition3 || condition4) { execute ..} else { execute..} |
One can use Switch in place of if-else. Switch can execute one statement for multiple conditions. It is an alternative of the if-else-if ladder condition. It also makes it easy to dispatch execution to different parts of code based on value of expression.
boolean result = (condition1 && condition2) && (condition3 || condition4) |
2.9 Use Primitive Types Wherever Possible
Java developers should try using primitive types over objects whenever possible as the data of primitive types are on stack memory. While on the other hand, if objects are used, they are stored on heap memory which is comparatively slower than stack memory.
For example: Use int instead of Integer, double instead of Double, boolean instead of Boolean, etc.
Apart from this, developers must also avoid default initialization values to assign while creating any variable.
3. Java Exception Handling Best Practices
Here are some of the best practices of Java Execution Handling -
3.1 Don’t Use an Empty Cache Block
Using empty catch blocks is not the right practice in Java programming and the reason behind it is that it can silently fail other programs or continue the program as if nothing has happened. In both cases, it makes it harder to debug the project code.
Here is an example showing how to multiply two numbers from command-line arguments - (we have used an empty catch block here)
// switch statement switch(expression) { // case statements // values must be of same type of expression case value1 : // Statements break; // break is optional case value2 : // Statements break; // break is optional // We can have any number of case statements // below is the default statement, used when none of the cases is true. // No break is needed in the default case. default : // Statements } |
Generally, the parseInt() method is used which throws a NumberFormatException Error. But in the above code, throwing exceptions has been ignored which means that an invalid argument is passed that makes the associated variable populated with the default value.
3.2 Handle Null Pointer Exception Properly
Null Pointer Exception occurs when the developer tries to call a method that contains a Null Object Reference. Here is a practical example of the same situation -
public class Multiply { 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) { // Throwing exception here is ignored } int multiply = a * b; System.out.println(a + " * " + b + " = " + multiply); } } |
Though there is no error in this code, if any method or object in this code is Null then the null pointer exception will be thrown by the code. In such cases, null pointer exceptions are known as inevitable methods but in order to handle them carefully, the developers must check the Nulls prior to execution as this can help them alter or eliminate the null in the code. Here is an example of the same -
int noOfStudents = office.listStudents().count; |
3.3 Use Finally Wherever Required
“Finally” block enables developers to put the important code safely. It enables the execution of code in any case which means that code can be executed whether exceptions rise or not. Besides this, Finally is something that comes with some important statements which are regardless of whether the exception occurs or not. For this, there are three different possibilities that can be carried out by the developers, and we will go through all these cases.
Case 1: Here, Finally can be used when an exception does not rise. The code written here runs the program without throwing any exceptions. Besides this, this code executes finally after the try block.
private int getListOfStudents(File[] files) { if (files == null) throw new NullPointerException("File list cannot be null"); |
Output -
inside try block
18
finally : always executes
Case 2: The second method where Finally is executed after the catch block when the exception rises.
// Java program to demonstrate // finally block in java When // exception does not rise import java.io.*; class demo{ public static void main(String[] args) { try { System.out.println("inside try block"); System.out.println(36 / 2); // Not throw any exception } // Not execute in this case catch (ArithmeticException e) { System.out.println("Arithmetic Exception"); } // Always execute finally { System.out.println( "finally : always executes"); } } } |
Output -
inside try block
catch : arithmetic exception handled.
finally : always executes
Case 3: The third case specifies the situation where the Finally is executed after the try block and is terminated abnormally. This situation is when the exception rises. In this case, in the end, Finally still works perfectly fine.
// Java program to demonstrate final block in Java // When exception rise and is handled by catch import java.io.*; class demo{ public static void main(String[] args) { try { System.out.println("inside try block"); System.out.println(34 / 0); // This Throw an Arithmetic exception } // catch an Arithmetic exception catch (ArithmeticException e) { System.out.println( "catch : arithmetic exception handled."); } // Always execute finally { System.out.println("finally : always executes"); } } } |
Output -
Inside try block
finally : always executes
Exception in thread "main" java.lang.ArithmeticException: / by zero
at demo.main(File.java:9)
3.4 Document the Exceptions Properly
Another best practice for Java exception handling is to document the exceptions of the project. This means that when any developer specifies any type of exception in the method, it must be documented. It helps the developers to keep records of all the information and also enables other team members to handle or avoid exceptions as per the requirement. And for this, the developer needs to add a @throws declaration in the Javadoc while documenting the exceptions and also describing the entire situation.
If you throw any specific exception, its class name should specifically describe the kind of error. So, you don’t need to provide a lot of other additional information. Here is an example for it -
import java.io.*; class demo{ public static void main(String[] args) { try { System.out.println("Inside try block"); // Throw an Arithmetic exception System.out.println(36 / 0); } // Can not accept Arithmetic type exception; Only accept Null Pointer type Exception catch (NullPointerException e) { System.out.println( "catch : exception not handled."); } // Always execute finally { System.out.println( System.out.println("finally : always executes"); } // This will not execute System.out.println("i want to run"); } } |
3.5 Do not Log and Rethrow the Exception
When any exception occurs in the application, it must be either logged or carried out with the app or rethrown and let another method be logged in to save the details. Both situations should never be carried out at the same time.
This means that the exceptions that developers carry out in the application must never log the details and then rethrow the same. Here is an example for the same -
/** * This method does something extremely useful ... * * @param input * @throws DemoException if ... happens */ public void doSomething(int input) throws DemoException { ... } |
3.6 Catch the Most Specific Exception First
Developers must catch the most specific exceptions first. By following this practice, code gets executed easily, and the catch block can be reached faster.
Here is an example of this where the first catch block enables the system to handle all NumberFormatExceptions and the second one enables handling of all IllegalArgumentExceptions that are not a part of NumberFormatException.
Other Generic Exceptions will be caught in the last catch block.
/* example of log and rethrow exception*/ try { Class.forName("example"); } catch (ClassNotFoundException ex) { log.warning("Class not found."); throw ex; } |
4. Java Logging Best Practices
Here are some of the best practices of Java Logging:
4.1 Use a Logging Framework
Using a logging framework is essential as it enables keeping records of all the methods and approaches of the code. And for robust logging, the developers must deal with concurrent access, format log messages, write alternative destinations of logs, and configure all the logs.
Basically, when the developer is adopting a logging framework, he will be able to carry out a robust logging process without any issues. One of the widely used logging frameworks is Apache Log4j 2 framework.
Additionally, you can use levels to control the granularity of your logging, for example: LOGGER.info, LOGGER.warn, LOGGER.error, etc.
4.2 Write Meaningful Messages
Another best practice while logging in to Java is to write meaningful messages. If the log events of the project contain meaningful and accurate messages about the given situation, it will be easy for the entire team to read and understand the code while working on it. And if any error occurs in the application, it can be really helpful as they will have enough information to understand and resolve the issue. Here is an example of the same -
public void catchMostSpecificExceptionFirst() { try { doSomething("A message"); } catch (NumberFormatException e) { log.error(e); } catch (IllegalArgumentException e) { log.error(e) } catch (Exception e) { log.error(e) } } |
The message must be written as -
LOGGER.warn("Communication error"); |
The first message will inform you that there is a communication issue in the logging process but it doesn’t specify anything else which means that the developer working at that time on the project will have to find out the context of the error, the name of the logger, and the line of the code that has a warning.
The second message provides all the information about the logging communication error which means that any developer will get the exact message that is required. This shows that when messaging is done in an easy way, it helps anyone to understand the error and the entire logging system.
4.3 Do not Write Large Logs
Writing large logs is not the right practice as when unnecessary information is incorporated it can reduce the value of the log as it masks the data that is required. It can also create problems with the bandwidth or the performance of the application.
Too many log messages can also make reading and identifying relevant information from a log file whenever a problem occurs.
4.4 Make Sure You’re Logging Exceptions Correctly
Another Java best practices that the developers must follow while logging the Java code is to make sure that the exceptions are logged correctly. They must not be reported multiple times. Exceptions must be only monitored and reported by using automated tools as they also create alerts.
4.5 Add Metadata
Adding metadata in the logs enables other developers working on the project to find production issues faster. Metadata can be really useful which means that the more they are used the more it is beneficial for the project.
Eg:
LOGGER.warn("Error while sending documents to events Elasticsearch server, response code %d, response message %s. The message sending will be retried.", responseCode, responseMessage); |
5. Conclusion
As seen in this blog, when any developer is developing a Java application, there are many things he must consider as in the long term other experts will be maintaining the same project. For this reason, the developer must create an application by following the Java best practices that are the standardized approaches for it. In this way, in the future, any other developer will be comfortable handling and maintaining the software development project.
View Comments
As someone who works with Java, this blog has incredibly benefited me. The in-depth explanations of essential coding practices have been constructive in solidifying my understanding and improving my coding skills. Thank you for sharing such invaluable content.
I gained valuable insights into a range of Java best practices from this blog, which will equip developers to build applications in a consistent manner. I'll be sure to share this informative article with my colleagues.