IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
Java Best Practices
Good code follows certain rules, and knowing them increases your chances of success. We’d like to share some Java best practices that will help you on your way. We’ll cover the must-know tips and tricks, from broad advice on software development to Java- and project-specific know-how. Let’s get started!
General
Let’s keep the following general rules about modern coding in mind.
Be clear, not clever
The primary purpose of code is to be understood and maintained, not to serve as a showcase of technical skills. Clear code leads to software that’s easier to debug, maintain, and extend, benefiting everyone involved in the project. Complexity isn’t a badge of honor – simplicity and readability are.
Consider the following examples.
- Clever:
This is an unconventional way to swap the values of variables `a` and `b`. Though it’s clever, it can be confusing to understand at first glance.
- Clear:
This is a more common approach. Even though it requires an additional line of code, its straightforwardness makes it easier to understand for most programmers.
Keep it short
Make sure your methods and classes aren’t too long. While there’s no strict rule regarding the exact number of lines or words for a class, it’s advisable to maintain a focused and cohesive structure. When it comes to methods, it’s generally suggested to aim for around 10 to 20 lines of code per method. If a method gets longer, it might be better to split it into smaller, more manageable parts.
If you’d like to practice recognizing methods that are too long, here is a great video by technical coach Emily Bache.
IntelliJ IDEA can also help you get rid of long methods and classes by providing various refactoring options. For example, it allows you to extract methods to break up a long method into shorter ones.
Naming is hard, so do it with care
Proper names for methods and variables serve as direct, intuitive guides to understanding the purpose and function of your code, which is essential for effective communication. Here is what you need to know about the most crucial naming conventions.
We advise staying away from single-letter variables, ensuring method names reflect their actions and aligning object and field names with the business domain to enhance clarity and communication. For instance, a method named calculateTotalPrice() instantly conveys its purpose, whereas a vague name like calculate() leaves its functionality ambiguous. Similarly, a variable named customerEmailAddress is clear right away, while an abbreviation like cea could refer to anything, leading to confusion.
As another example, instead of naming a variable timeout, it’s helpful to specify the unit to which it relates. Use timeoutInMs or timeoutInMilliseconds to avoid confusion about the units.
Test, test, test
Testing your code is necessary to ensure your app works as expected and will continue to work as it changes. Tests help spot issues early, making fixes cheaper and easier. They also guide you on how the code should work and minimize the possibility of breaking it when updating it later.
Good test names are important as they show what each test does and what it looks for. For example, a test called AlertWhenEmailIsMissing()checks for alerts when an email is missing, so you don’t need to dig into the details.
You can read this blog post by Marit van Dijk for more information on tests.
Language-specific
The following tips and tricks will help you elevate your code by avoiding some common mistakes while writing in Java.
Use switch expressions instead of excessive If statements
Using switch expressions can make your code more readable and organized by consolidating multiple conditions into a single structure. This approach simplifies the code, making it easier to understand and maintain.
Let’s consider the following example with different types of ice cream and their key ingredients.
- Excessive else-if
In this example, the code uses a chain of else–if statements to match an ice cream flavor with its key ingredient. As the number of flavors grows, the number of if statements can become unmanageable, making the code harder to read.
- Switch
In this alternative example, we achieve the same result using a switch expression instead of multiple if-else conditions. The switch expression is more compact, neater, and more easily understood when comparing a single variable against multiple constant values.
IntelliJ IDEA offers a special inspection that can transform your if statements into switch expressions in a matter of seconds.
You can find more amazing examples of switch usage in a recent blog post by Java Developer Advocate Mala Gupta.
Avoid empty catch blocks
Empty catch blocks in Java are catch clauses that don’t have any code inside them to handle exceptions. When an exception is caught by such a block, nothing happens and the program continues as if no error occurred. This can make it difficult to notice and debug issues.
- Empty Catch Block
In this scenario, we catch the exception but do nothing about it.
IntelliJ IDEA highlights such cases with an inspection and offers solutions:
- Logging the exception
One way of handling the exception is to log it with the help of e.printStackTrace(), which prints the stack trace to the console, helping us identify and debug potential issues.
- Logging the exception and rethrowing it
In an ideal world, the catch block would identify IOException, then print an error message to the console and rethrow the exception to handle it later.
This way, you get the full picture of what went wrong.
- Logging the exception and returning an alternate value
Another way of solving the empty catch block issue is by logging the exception but returning a meaningful value.
Choose collections over arrays for greater flexibility
While arrays in Java are efficient and easy to use, they are also fixed in size and offer limited operations, making them less adaptable for various data manipulations.
Collections in Java provide far more flexibility and utilities, such as ArrayList or HashSet. For example, ArrayList offers dynamic resizing and many utility methods, and is easier to work with, especially with generics. Let’s take a look at this through some code examples:
- Arrays
We’ve created an array of Strings. Since arrays in Java are fixed size, if we wanted to add an eleventh element, we’d have to create a new array and copy over all of the elements.
- Collections
As an alternative to the previous code, we can use a collection class called ArrayList. Collections like ArrayList can grow and shrink at runtime, providing greater flexibility. They also come with powerful methods to manipulate data, such as .add(), .remove(), .contains(), .size(), and more.
Embrace immutability
Immutable objects are objects whose state cannot be changed after creation. They help to write safer and cleaner code by removing the complexities associated with tracking mutable state changes. This minimizes the risk of bugs and unintended side effects, ensures consistent behavior, and simplifies the process of debugging and maintaining the application. In Java, we achieve immutability using final.
- No final
In this example, we’ll create a Car class and print its brand and model. After that, we’ll change the car’s model and print it again. The console output will show that the state of the Car class has been changed, illustrating mutable behavior.
- With final
In the improved code below, you can see that we’ve done the same, but we can’t change the car model or brand because we don’t have a setter method and the Car class is final. The console output will show that the state of the Car class remains the same, illustrating immutable behavior.
Favor composition over inheritance
In Java, it’s usually better to use composition (having a reference or dependency on an object of another class) instead of inheritance (creating a subclass from a superclass). Composition makes the code more flexible and easier to test.
- Inheritance
In our example, GamingComputer inherits from BasicComputer. This can cause problems because if BasicComputer changes, it might break GamingComputer. Also, GamingComputer is locked into being a type of BasicComputer, limiting its flexibility.
- Composition
In the second example, the Computer class is composed of Memory and Processor which are separate class instances that act as fields. Each of these classes defines its methods and behaviors independently. This approach is more flexible as you can swap different Memory or Processor types or change their behavior at runtime without changing the Computer class.
For more examples, take a look at Easy Hacks: How to Create Inheritance in Java.
Streamline functional interfaces with lambdas
Functional interfaces in Java are a type of interface that has just one abstract method. Lambdas provide a sleek, expressive way to implement them without the boilerplate code of anonymous classes.
- Without lambdas
Below we’ll use an anonymous inner class to implement the Comparator interface for sorting. This is bulky and may become unreadable with more complex interfaces.
- With lambdas
This code does the same as above, but we are using a lambda function instead of an anonymous class. This makes the code much more compact and intuitive.
Use enhanced for loops or streams
Enhanced for loops (for-each loops) and streams in Java offer more readable and compact ways to iterate over collections or arrays compared to a traditional for loop.
- Classic for loop
In this code, we’re using a for loop to go over the list. It requires a counter, handling the element index, and defining the stopping condition – all of which add to the complexity.
- Enhanced for loop
This loop (forEach) eliminates the need for counters and directly gives us each item in the list, simplifying code and reducing the potential for errors. You can apply it with the help of an inspection in IntelliJ IDEA.
- Streams
The usage of streams gives us each item, like the enhanced for loop, but also allows us to do complex operations like filtering and mapping.
Safeguard resources with try-with-resources statements
Try-with-resources statements help you ensure that each resource is closed properly after use. Not closing resources in a try block can cause memory issues and application errors, impacting performance and reliability.
- Closing resources manually
In the example below, we handle the system resource FileInputStream manually, which may lead to a resource leak and other problems if an exception is thrown while closing.
- Using try-with-resources statements
In the improved version, declaring FileInputStream inside the try block means Java will automatically close it, regardless of whether we leave the try block normally or with an exception.
Untangle deeply nested code
Deeply nested code highlights logical issues that can be difficult to notice initially. This often comes from using lots of conditionals, which are the coding essentials, so we can’t just get rid of them. However, we do need to find ways to simplify the code.
- Numerous conditionals
You can see how strange abundant nested conditionals look.
- Refactored code
We’ve used a “guard clauses” technique to eliminate nested conditionals. By quickly exiting the function when certain conditions are met, we preserve the same logic with cleaner-looking code.
Project-specific
We also have some helpful dos and don’ts for working on projects that contain multiple dependencies.
Keep dependencies up to date
Remember to keep your project’s dependencies updated to enhance security, introduce new features, and fix bugs. Regular updates ensure your project runs smoothly and remains compatible with other tools.
IntelliJ IDEA can help you keep your dependencies up to date. First, install the Package Search plugin from JetBrains Marketplace via Preferences/Settings | Plugins. Then navigate to the Dependencies tool window to see all of the existing dependencies in your project and click the Upgrade link next to them.
Check for vulnerable dependencies and APIs
Regularly scanning your project for weak spots in dependencies and APIs helps you minimize security risks, stick to predefined rules, and keep things running smoothly. Addressing these vulnerabilities quickly helps protect your project and its users from potential threats.
To find vulnerable dependencies in IntelliJ IDEA, navigate to Code | Analyze Code and select Show Vulnerable Dependencies. The findings will appear in the Vulnerable Dependencies tab of the Problems tool window.
You can also right-click a folder or file, like pom.xml or build.gradle, in the Project tool window and choose Analyze Code | Show Vulnerable Dependencies from the context menu.
Even without explicitly checking for vulnerabilities, IntelliJ IDEA will highlight vulnerabilities in pom.xml or build.gradle.
Avoid circular dependencies
Circular dependencies happen when parts of your project rely on each other in a loop. For example, part A needs something from part B, but part B also needs something from part A. This can make your project messy and hard to work on because it’s tough to figure out where one part starts and another ends. It’s best to avoid these loops to keep things clear and easy to manage.
To avoid circular dependencies, you can use the Dependency Matrix. It helps you visualize dependencies between components in your projects.
Conclusion
We hope that our advice simplifies your daily tasks and encourages you to write simple, clear, and professional code, inspiring you to become a more efficient developer.
IntelliJ IDEA will help you find many of the items discussed in this blog post and provide automatic fixes. Give it a try and let us know what you think!