Java Best Practices [Part 2]
Welcome back to our blog on Java best practices. In our previous post, we talked about the practices of writing clean code and described what are the benefits of employing these practices. If you missed our last article, you can read it here.
Recap: What is Clean Code and Why Does it Matter?
Clean code is the practice of writing code that is easy to read, understand, and maintain. When your code is well-organized and follows established standards, it communicates its intent clearly without the need for extensive explanations. Clean code offers numerous benefits, as mentioned below.
Readability: Clean code follows standards and norms that make it a breeze to read and understand. No more squinting at the screen and scratching heads!
Maintainability: Keeping your code clean enhances its maintainability. So when the time comes for updates and tweaks, you won’t be diving into a code jungle with a machete.
Collaboration: Clean code brings harmony to team collaborations. Multiple developers can work together without stepping on each other’s toes.
Debugging Magic: With clean code, debugging becomes a walk in the park. The clues are all laid out, making it easier to chase those pesky bugs away.
Peace of Mind: Ahh, the blissful feeling of knowing your code is a work of art — clean, bug-free, and ready to take on the world!
Let’s dive right into the second part of our Java best practices journey. In this part, we will be exploring some more methods that will level up your Java coding game.
1. Avoid Redundant Initialization
Avoid unnecessary assignments to variables, as Java automatically initializes primitive types and reference types with default values when they are declared as class or interface fields. For instance:
Boolean fields are automatically set to false.
Fields of types byte, short, int, long, char, and float are initialized to zero.
Double fields are initialized to zero, represented as 0.0d.
Reference fields are initialized to null.
Hence, it’s advisable to avoid explicitly initializing these fields to their default values, as it becomes redundant and unnecessary. Consider the following examples:
// Bad Practice: Redundant Initialization
public class Employee {
// Redundant initialization
private String name = null;
private int age = 0;
private boolean active = false;
}
// Good Practice: No Initialization Needed
public class Employee {
// No initialization needed
private String name;
private int age;
private boolean active;
}
The redundant initialization adds extra code without changing the class’s behavior or state. In contrast, the good practice of omitting the initialization makes the code more concise and clear.
Keep in mind that this rule applies only to “fields” of a “class” or an “interface.” Local variables declared within a method or block must be explicitly initialized before use to avoid compile-time errors.
2. Minimize Mutability
Mutability refers to the ability of an object or data to be changed after its creation. While it has its advantages, it also introduces challenges, such as difficult to debug program. Therefore, it’s important to use mutability carefully and consider the trade-offs between mutability and immutability based on the specific requirements of your program.
Immutability is a powerful technique that helps reduce bugs caused by unexpected changes. Hence, it is good practice to minimize mutability in classes and variables. You can use the “final” keyword for constants and variables that should remain unchanged.
final int MAX_ATTEMPTS = 3;
final String API_KEY = “abc123”;
3. Handle Exceptions Properly
Use exception handling to handle errors and exceptional cases.
Use logging to record errors and other important events.
Use clear and informative error messages.
No Empty Catch Blocks: When you are programming, exceptions, or unexpected events, are inevitable. There are various methods exist to deal with unexpected events. An empty block in Java is used for error handling, but it allows the program to continue running without throwing an error.
This means that when an exception is thrown in the try block, the catch block simply catches the exception and does nothing else. Therefore, you should avoid using empty catch blocks, which can lead to the silent failure of the program.
In this example, catch block is empty, and when we run this method with the following parameter [“123”, “1abc”]
The method will fail without any feedback. It will return 123, even though there was an error. This is because the exception is ignored. It’s important to handle exceptions so that we know when something goes wrong. Ignoring exceptions can lead to silent failures, which can be difficult to debug further.
Always handle exceptions appropriately to prevent unexpected crashes and ensure graceful error recovery. Use specific exception types rather than catching generic Exception classes. For instance:
Consider the following code snippet:
public int aggregateIntegerStringss(String[] integers) {
int sum = 0;
try {
for (String integer : integers) {
sum += Integer.parseInt(integer);
}
} catch (NumberFormatException ex) {
}
return sum;
}
In this example, catch block is empty, and when we run this method with the following parameter [“123”, “1abc”]
The method will fail without any feedback. It will return 123, even though there was an error. This is because the exception is ignored. It’s important to handle exceptions so that we know when something goes wrong. Ignoring exceptions can lead to silent failures, which can be difficult to debug further.
Always handle exceptions appropriately to prevent unexpected crashes and ensure graceful error recovery. Use specific exception types rather than catching generic Exception classes. For instance:
// Avoid
try {
// Risky code
} catch (Exception e) {
// Exception handling
}
// Prefer
try {
// Risky code
} catch (IOException e) {
// IOException handling
}
4. Optimize I/O operations:
Minimize the number of I/O operations and use buffered streams or readers/writers for efficient reading and writing of data.
// Avoid
FileInputStream fis = new FileInputStream(“myfile.txt”);
int data;
while ((data = fis.read()) != -1) {
// Process data
}
fis.close();
// Prefer
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(“myfile.txt”));
int data;
while ((data = bis.read()) != -1) {
// Process data
}
bis.close();
5. Tips to write well-performance code:
- Use parallel processing when applicable: Take advantage of parallel streams or multi-threading to parallelize computationally intensive or I/O-bound tasks, improving overall performance. However, ensure proper synchronization and handle thread safety.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream().mapToInt(Integer::intValue).sum();
Use records: Records to create immutable data classes that automatically generate useful methods like equals, hashCode, and toString.
Compare constants: Constant should always be first in comparison in equals to avoid null pointer exception.
// Bad Practice
user.getRole().equals(UserRole.ADMIN);
// Good Practice
UserRole.ADMIN.equals(user.getRole());
- Avoid if else: You can see how we have avoided writing the else part, and the code looks clean.
The following code can be improved:
if(user.isAdmin){
// execute code
} else {
throw error
}
Like this:
if(!user.isAdmin){
throw error
}
//execute code
- Long Methods: Making Things Short and Sweet: We can split long methods into smaller ones, each with its own job. This makes the code easier to understand. We can also use a trick called AOP(Aspect-Oriented Programming) to help with some tasks that happen a lot.
// Bad Practice
public class LongMethod {
public void complexTask() {
// A lot of steps here
}
}
Break down long methods into smaller ones.
// Good Practice
public class MethodBreakdown {
public void firstStep() {
// First step logic
}
public void secondStep() {
// Second step logic
}
// …
}
- Divide responsibilities of the controllers: We can make things easier by splitting big controllers into smaller ones. Each smaller controller can handle just one job. This makes everything clearer and tidier. We can use special marks like @RestController and @RequestMapping to do this.
// Bad Practice
@RestController
public class MainController {
@GetMapping(“/api/items”)
public List<Item> getItemsAndPerformOtherTasks() {
// Multiple tasks done here
}
}
Divide responsibilities between controllers.
// Good Practice
@RestController
@RequestMapping(“/api”)
public class ItemController {
@GetMapping(“/items”)
public List<Item> getItems() {
// items logic
}
}
@RestController
@RequestMapping(“/api”)
public class TaskController {
@GetMapping(“/tasks”)
public List<Task> getTasks() {
// tasks logic
}
}
6. Code formatting
Proper code formatting is one of the key aspects of writing clean code. Consistent formatting promotes better readability of the codebase. Regarding formatting, indentation, blank lines, and line length are crucial aspects of code formatting. The proper indentation, character limit and separate blocks make the codebase pleasant to read.
Indent code with four spaces.
Limit line length to 80 characters.
Use blank lines to separate blocks of code.
Read more at https://www.brilworks.com/blog/java-best-practices-part-2/
Subscribe to my newsletter
Read articles from Brilworks Software directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Brilworks Software
Brilworks Software
Brilworks is a software and mobile app development company that builds custom software and apps for startups & enterprises to boost business performance and productivity.