8+1 Tips to Optimize your Java Code

Thanasis Galatis
9 min readSep 20, 2022

As computer scientist Donald Knuth, famously said: “Premature optimization is the root of all evil.” It should be noted that by optimizing, you usually make your code more difficult to read and even worse it is highly possible to introduce new bugs. Generally speaking, using better algorithms (with decreased time complexity) and choosing appropriate collection types will yield a bigger performance increase most of the times than any amount of low-level optimizations. The high level optimizations are also technology-agnostic and in this way it is more likely to deliver an improvement under every execution condition. So prefer, high level optimizations over lower level ones, where possible.

Optimization techniques

There are many common optimization methods you can apply regardless of the language you use. While working on Java, we often come across the concept of optimization, too. Code cleanliness is more important, but the time taken by the code to execute should be within acceptable limits. Here are 7+1 simple tips :) to help you achieve your optimization goals.

1. Strength reduction and binary operators

The most common example of strength reduction is using the shift operator to multiply and divide integers by a power of 2. For example, x >> 2 can be used in place of x / 4 and x << 1 can replace x * 2 etc.

But, note that compilers are currently able to optimize x / 4 into x >> 2, using strength reduction, and even some of the oldest compilers can do this. So there may be no benefit to writing x / 4 as x >> 2, but in many other examples, this technique may get handy.

Generally, binary operators make your code more performant in most occasions. For example, if you need to check for even numbers,

change:

if (number % 2 == 0) { ... } 

to:

if ((number & 1) == 0) { ... }

x & 1 produces a value that is either 1 or 0, depending on the least significant bit of x. If the last bit is 1, the result of x & 1 is 1; otherwise, it is 0. This is a bitwise AND operation. See more bitwise operations here

2. Common sub expression elimination

Do you execute the same calculation again and again? That takes time. So by using common sub expression elimination, it means you have to remove all the redundant calculations and make the calculation once.

Instead of writing:

double vatAmountA = grossAmountA / (1 + (1 / vatAmountPercent));
double vatAmountB = grossAmountB / (1 + (1 / vatAmountPercent));

the common sub expression is calculated once and used for both results:

double v = (1 + (1 / vatAmountPercent));
double vatAmountA = grossAmountA / v;
double vatAmountB = grossAmountB / v;

3. Avoid multiple if-else statements

An if-else is a common form of conditional statement in programming; It tells the computer that if certain conditions are met, make some actions. Else, if the conditions are false, do another thing. But, if we are using too many conditional if-else statements it will have an impact on performance since JVM will have to compare the conditions (too many if statements impact code cleanliness too). This can get even worse if statements are overused in loops like for, while, etc. If there are too many conditions in your business logic, try to group the conditions and escape early. You may also extract the outcome and return it or use it in the if statement. Moreover, if there is a defined set of options, think about using a switch statement, which has a slight performance advantage over if — else.

Example:

public boolean foo() {
if (condition1) {
if (condition2) {
if (condition3 || condition4) {
return true;
} else {
return false;
}
}
}
return false;
}

Solution1: Avoid the above, by using return to escape early (also makes code more clean);

public boolean foo() {    if (!(condition1 && condition2)) return false;
if (!(condition3 || condition4)) return false;
// Otherwise
return true;
}

Solution2: Even better, use a variable :

boolean result = (condition1 && condition2) && (condition3 || condition4)

or extract the logic to a function:

public boolean conditionsMet() {
return (condition1 && condition2) && (condition3 || condition4);
}

4. Code motion

When code consists of invariant statements or expressions whose result doesn’t change, this part of code can be moved at the top, outside the body of the loop, so that it is only executed when the result changes, rather than executing each time the result is required. This is most common with loops, but it can also involve code repeated on each invocation of a method. The following is an example of invariant code motion in a loop:

for (int i = 0; i < arr.length; i++) {
arr[i] += i + (2 * y) * Math.sin(y);
}

Solution: Extract variables and move the code that performs the operation to the top. Also insert the array length calculation in a variable.

double twoYsinY = (2 * y) * Math.sin(y);
int arrLength = arr.length;
for (int i = 0; i < arrLength; i++) {
arr[i] += i + twoYsinY;
}

5. Make optimizations in loops

Many, if not all, performance problems involve some kind of loops, so you can get a great performance boost starting by them. The impact of the loop overhead depends on the number of iterations the loop executes, how many are those and how much work it does during each iteration.

for (i = 0; i < arrLength; i++) {
// Do something
}

If it isn't necessary inside the loop for the loop variable i to count from 0 to arrLength - 1, restructure the loop in order to check against 0, which is almost always more performant in any language. Rather than comparing i against arrLength at each iteration, which requires arrLength to be loaded (assuming it isn't a constant variable). To optimize, restructure the loop as:

for (i = arrLength; --i >= 0; ) {
// Do something
}

By combining the pre-decrement of i with the comparison against 0, the necessary condition flags for the comparison will already be set, so no explicit comparison is required.

If we want to iterate over an array, we can make our loop even more performant by correct exception handling. Note that Java typically performs bounds checks for all array accesses, so you can rely on this fact and simply catch the ArrayIndexOutOfBoundsException, which will be thrown when the loop tries to iterate past the end of the array.

try {
for (i = 0; ;i++)
array[i] = i;
} catch (ArrayIndexOutOfBoundsException e) {}

Keep in mind that, it is not cheap to throw an exception and the iterations should cover this cost, so be careful. The above isn’t also a very good coding style but that’s the price you have to pay for optimizations.

6. Don’t get the size of the collection during the loop

One more about loops! While iterating through any collection, you may get the size of the collection beforehand and not during iteration. In this way the length is calculated beforehand and Java doesn’t need to reload every time, as a local variable cannot be changed by the method call and/or another thread. Local variables can be changed only inside the method itself, and Java can now know with certainty that variable listSize will not change. An example is provided below:

From this:

List<String> myList = getItemsData();
for (int i = 0; i < myList.size(); i++) {
// Do something ..
}

To this:

List<String> myList = getItemsData(); 
int listSize = myList.size();
for (int i = 0; i < listSize; i++) {
// Do something ..
}

7. Unrolling loops

Unrolling loops means to simply perform more than one operation each time through the loop in each iteration. In this way, loop executes fewer iterations. For example, in the code below, insert operation is executed 100 times, but we can modify the code to execute only 20 iterations, instead of the previous 100.

Normal loop

int x;
for (x = 0; x < 100; x++) {
insert(x);
}

After loop unrolling

int x; 
for (x = 0; x < 100; x += 5) {
insert(x);
insert(x + 1);
insert(x + 2);
insert(x + 3);
insert(x + 4);
}

After unrolling, only 20% of the jumps and conditional branches need to be taken. That’s a potentially significant decrease in time complexity.

On the other hand, this manual loop unrolling expands the code size from 3 lines to 7, that have to be debugged once more, and the compiler may have to allocate more registers to store variables in the expanded loop iteration. In addition, the loop control variables and number of operations inside the unrolled loop structure have to be chosen carefully so that the result is indeed the same as in the original code. For example, consider the implications if the iteration count were not divisible by 5.

8. Don’t use “+” for Concatenation

A string in Java is immutable. We cannot change the object itself, but we can change the reference to it. Every time we concatenate with “+”, a new String object will be created. So if we need to create a large string in the case of SQL queries etc., it is sometimes better not to concatenate the String object using the ‘+’ operator, as this will lead to multiple objects of String created, leading to more usage of heap memory. In this case, we can apply other solutions like:

  1. Use concat() method instead. concat() method is more performant than the + operator because it creates a new object only when the string length is greater than zero(0) but the + operator always creates a new string irrespective of the length of the string.
  2. Use StringBuilder or StringBuffer. The former is preferential over the latter since it has a performance advantage due to non-synchronized methods. A sample is provided below:

Sample:

String query = String1 + String2 + String3;

Instead you may use StringBuilder as follows:

StringBuilder strBuilder = new StringBuilder("");
strBuilder.append(String1).append(String2).append(String3);
String finalString = strBuilder.toString();

9. Be careful of autoboxing and avoid wrappers when you don’t need them

And the last one is about (w)rappers :) Wrapper classes provide a way to use primitive data types (int, double, boolean etc.) as objects. But wrapping a primitive can waste time, as a wrapper object has to be converted to a primitive and vice versa. If you need an int from an Integer for example, you must unbox the Integer using the intValue method. This process is hopefully automatic with auto unboxing but that doesn’t remove the performance hit. Moreover, after some action is performed the result is then wrapped in a new Integer class (autoboxing), and all this happens at runtime. To read more for autoboxing click here. So, if possible, use primitive types instead of objects, int over Integer, double over Double and so on.

Which of the 2 samples below will run faster and how much is the difference?

Sample 1

int sum = 0;
for (int i = 0; i < 1_000_000_000; i++) {
sum += i;
}

Sample 2

Integer sum = 0;
for (int i = 0; i < 1_000_000_000; i++) {
sum += i;
}

Tests

With primitives only

Run 1
455808800 ns.
Run2
457606400 ns.
Run3
456544700 ns.
Run 4
453318600 ns.

With wrappers

Run 1
4256561300 ns.
Run 2
4099268700 ns.
Run 3
3974346300 ns.
Run 4
3988208600 ns.

That’s an average of 455819625 ns. (0,46 s.) for the first one on my computer and an average of 4079596225 ns. (4,08 s.) for the second sample. The first one is almost 9 times faster than the second one on my PC (AMD Ryzen 5 4600H 3.00 GHz / 16GB ram) with the only difference being the use of a wrapper class instead of a simple int primitive. The reason for this is that the sum variable has to be converted to a primitive int (auto unboxing) and after the addition is performed the result is then wrapped in a new Integer class (autoboxing), which means that the program constructs about 1 billion unnecessary Integer instances (roughly one for each time the int i is added to the Integer sum) and perform 2 billion boxing operations, more. This has a tremendous impact on performance.

Primitives vs Wrapper Objects — General performance

The image below shows the general performance impact, since primitives live in the stack and hence are accessed fast whereas wrappers are objects and they live on the heap and are relatively slower to access in comparison to their primitive counterparts.

Source: https://www.baeldung.com/java-primitives-vs-objects

Further Reading & References:

What’s next? Follow me on Medium to be the first to read my stories.

--

--

Thanasis Galatis

Software Engineer with a passion to learn and share programming and IT knowledge.