Things to avoid while writing Java

Borislav Stoilov
6 min readJun 6, 2022

--

Code background

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.

--

--