Things to avoid while writing Java
A short article listing mistakes developers make when writing Java. We want our code to be efficient and compatible. I will try to update the article based on people’s suggestions or things I find online overtime
Using Enum.values
This one caught me by surprise since I was using it on a regular basis. The problem with Enum.values() is that it has to be an immutable list by the specification. In order to achieve this, it returns a new array instance with the enum values every time it is called.
They are two separate objects in the memory, this might not sound like a big deal, but if you use Fruit.values() while processing a request for example and you have a high load this could lead to memory problems.
You can easily fix this by introducing a private static final variable VALUES to cache them.
Passing Optional as a method parameter
Consider the following
We pass the optional zoneId parameter and based on its presence we decide whether to give the time in the system time zone or use the specified zone. However, this is not how optionals should be used. We should avoid using them as a parameter and use method overloading instead.
This code is much easier to read and debug
Using StringBuilder
Strings in Java are immutable. This means that once created they are no longer editable. The JVM maintains a pool of strings and before creating a new one it calls the String.intern() method, which returns an instance from the string pool matched by value if such exists.
Let’s say we want to create a long string by concatenating things to it
Not so long ago we were told that this is a very bad idea because older versions of java did the following
- In line 1 the string ‘start’ is inserted in the string pool and longString is pointed to it
- In line 2 the string ‘startmiddle’ is added to the pool and longString is pointed to it
- In line 3 we have ‘startmiddlemiddle’
- In line 4 ‘startmiddlemiddlemiddle’
- and finally, at line 5, we add ‘startmiddlemiddlemiddleend’ to the pool and point longString to it
All of those strings stay in the pool and are never used, this wastes large amounts of RAM.
In order to avoid this, we can use StringBuilder
The StringBuilder creates only one string when the toString method is called, thus saving us all the intermediate strings that were initially added to the pool. However, after Java 5 this is done automatically for us by the compiler and it is safe to use string concatenation with ‘+’.
There is one exception to this rule and that is when you are doing string concatenation in a loop
This code won’t be optimized by the JIT and new strings will be inserted into the string pool every iteration, here we have to use StringBuilder
Few other things to note here
The Just-In-Time compiler will organize the code sometimes.
String s = "1" + "2" + "3";
Is converted to
String s = “123”;
Since Java 15 you can use text blocks, very useful for multi-line strings:
Using primitive wrappers when you don’t need them
Consider the following two snippets
The first one is 6 times faster than the second one on my machine. The only difference is that we use the wrapper Integer class. The reason for this is that in line 3 the runtime has to convert the sum variable to primitive int (auto unboxing) and after the addition is performed the result is then wrapped in a new Integer class (autoboxing). This means we create 1 million Integer classes and perform 2 million boxing operations, which explains the drastic slowdown.
Wrapped classes should be used only when they need to be stored in collections. However, future Java versions will support Collections of primitive types, which will make the wrappers obsolete.
Writing your own hash functions
Hash functions are typically used when you want to store your object in a HashMap. The map is composed of ‘buckets’ with a number and each hash code is assigned to a particular bucket. If your hash function is not properly written the performance of the map will degrade significantly. A well-written hash function will ensure equal distribution across all keys and this is not a trivial thing to achieve.
There are cases where you would write the hash function yourself but for the most part, you should use the built-in Objects.hash method
For immutable objects, it makes sense to cache the hash value, so it doesn’t get recalculated every time.
Using java.util.Date
You should even avoid all of the time classes in java.util use the java.time package instead.
The Date class is deprecated for many reasons and it has many design flaws.
- It is not immutable
- It can’t handle timezones
- Full of legacy code that is deprecated but still used
The Date, Calendar, and the rest time classes in util were rushed when the need for date support arose in the language. There were a few attempts to fix them but in the end they decided to introduce a new package java.time. The java.time package is very similar to the joda.time, which is a third party, this means that you also don’t need to use joda.time since you already have built-in support.
Let's list the three most important classes you will be using from java.time
LocalDate
Represents a date (without the time of day) in a particular timezone.
LocalDateTime
Same as LocalDate but it has the time of day.
Instant
It essentially is LocalDateTime but forced to UTC timezone. When dealing with timezones in your application it is a good idea to have a single zone across all services and databases. When you use Instant everything becomes UTC and then readers can convert it to a different zone if they chose to.
Summary
- Don’t use Date and Calendar (or anything date related from java.util)
- Don’t use joda.time (because it is very similar to java.time)
- Use LocalDate if you are interested only in the Date at a zone
- Use LocalDateTime if you are interested in the date and time at a zone
- Use Instant if you need date-time and don’t want to deal with timezones
Don’t do IO in parallel streams
In Java parallel streams use the ForkJoinPool under the hood to run its tasks. ForkJoinPool is a thread pool especially designed for compute tasks (CPU heavy tasks). Its size is always equal to the number of CPU cores your system has.
This pool is shared across the whole process and you gain easy access to it via the parallel stream API. IO bound operations block the thread they are running on making it unavailable for others until the blocking operation completes. This could potentially devastate the performance of the whole application if you put too many blocking calls inside the pool. Many internal Java tasks rely on the ForkJoinPool, so you should always think twice before using the parallel stream API.
Be careful with the orElse statement of the Stream API
The operation in orElse calls is always executed regardless of whether its needed for the final result or not.
Consider the following code
The output is what we would expect
Performing a very heavy request
DATA
But if we give value to the input variable
We get this output
Performing a very heavy request
some data we've inputed
The heavy request was executed, but we didn’t want to use its result. As a rule of thumb avoid performing IO bound operations in orElse statements.
To fix the issue from above we have to use orElseGet
String result = Optional.ofNullable(input).orElseGet(() -> other());
This way the other() method is called only if needed.