By Mikhail Selivanov – Freelance Software Engineer at Toptal

Java is a programming language that was initially developed for interactive television, but over time it has become widespread over everywhere software can be used. Designed with the notion of object-oriented programming, abolishing the complexities of other languages such as C or C++, garbage collection, and an architecturally agnostic virtual machine, Java created a new way of programming. Moreover, it has a gentle learning curve and appears to successfully adhere to its own moto – “Write once, run everywhere”, which is almost always true; but Java problems are still present. I’ll be addressing ten Java problems that I think are the most common mistakes.

Common Mistake #1: Neglecting Existing Libraries

It’s definitely a mistake for Java Developers to ignore the innumerable amount of libraries written in Java. Before reinventing the wheel, try to search for available libraries – many of them have been polished over the years of their existence and are free to use. These could be logging libraries, like logback and Log4j, or network related libraries, like Netty or Akka. Some of the libraries, such as Joda-Time, have become a de facto standard.

The following is a personal experience from one of my previous projects. The part of the code responsible for HTML escaping was written from scratch. It was working well for years, but eventually it encountered a user input which caused it to spin into an infinite loop. The user, finding the service to be unresponsive, attempted to retry with the same input. Eventually, all the CPUs on the server allocated for this application were being occupied by this infinite loop. If the author of this naive HTML escape tool had decided to use one of the well known libraries available for HTML escaping, such as HtmlEscapers from Google Guava, this probably wouldn’t have happened. At the very least, true for most popular libraries with a community behind it, the error would have been found and fixed earlier by the community for this library.

Common Mistake #2: Missing the ‘break’ Keyword in a Switch-Case Block

These Java issues can be very embarrassing, and sometimes remain undiscovered until run in production. Fallthrough behavior in switch statements is often useful; however, missing a “break” keyword when such behavior is not desired can lead to disastrous results. If you have forgotten to put a “break” in “case 0” in the code example below, the program will write “Zero” followed by “One”, since the control flow inside here will go through the entire “switch” statement until it reaches a “break”. For example:

public static void switchCasePrimer() {
    	int caseIndex = 0;
    	switch (caseIndex) {
        	case 0:
            	System.out.println("Zero");
        	case 1:
            	System.out.println("One");
            	break;
        	case 2:
            	System.out.println("Two");
            	break;
        	default:
            	System.out.println("Default");
    	}
}

In most cases, the cleaner solution would be to use polymorphism and move code with specific behaviours into separate classes. Java mistakes such as this one can be detected using static code analyzers, e.g. FindBugs and PMD.

Common Mistake #3: Forgetting to Free Resources

Every time a program opens a file or network connection, it is important for Java beginners to free the resource once you are done using it. Similar caution should be taken if any exception were to be thrown during operations on such resources. One could argue that the FileInputStream has a finalizer that invokes the close() method on a garbage collection event; however, since we can’t be sure when a garbage collection cycle will start, the input stream can consume computer resources for an indefinite period of time. In fact, there is a really useful and neat statement introduced in Java 7 particularly for this case, called try-with-resources:

private static void printFileJava7() throws IOException {
    try(FileInputStream input = new FileInputStream("file.txt")) {
        int data = input.read();
        while(data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
}

This statement can be used with any object that implements the AutoClosable interface. It ensures that each resource is closed by the end of the statement.

Common Mistake #4: Memory Leaks

Java uses automatic memory management, and while it’s a relief to forget about allocating and freeing memory manually, it doesn’t mean that a beginning Java developer should not be aware of how memory is used in the application. Problems with memory allocations are still possible. As long as a program creates references to objects that are not needed anymore, it will not be freed. In a way, we can still call this memory leak. Memory leaks in Java can happen in various ways, but the most common reason is everlasting object references, because the garbage collector can’t remove objects from the heap while there are still references to them. One can create such a reference by defining class with a static field containing some collection of objects, and forgetting to set that static field to null after the collection is no longer needed. Static fields are considered GC roots and are never collected.

Another potential reason behind such memory leaks is a group of objects referencing each other, causing circular dependencies so that the garbage collector can’t decide whether these objects with cross-dependency references are needed or not. Another issue is leaks in non-heap memory when JNI is used.

The primitive leak example could look like the following:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);

scheduledExecutorService.scheduleAtFixedRate(() -> {
	BigDecimal number = numbers.peekLast();
   	if (number != null && number.remainder(divisor).byteValue() == 0) {
     	System.out.println("Number: " + number);
		System.out.println("Deque size: " + numbers.size());
	}
}, 10, 10, TimeUnit.MILLISECONDS);

	scheduledExecutorService.scheduleAtFixedRate(() -> {
		numbers.add(new BigDecimal(System.currentTimeMillis()));
	}, 10, 10, TimeUnit.MILLISECONDS);

try {
	scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
	e.printStackTrace();
}

This example creates two scheduled tasks. The first task takes the last number from a deque called “numbers” and prints the number and deque size in case the number is divisible by 51. The second task puts numbers into the deque. Both tasks are scheduled at a fixed rate, and run every 10 ms. If the code is executed, you’ll see that the size of the deque is permanently increasing. This will eventually cause the deque to be filled with objects consuming all available heap memory. To prevent this while preserving the semantics of this program, we can use a different method for taking numbers from the deque: “pollLast”. Contrary to the method “peekLast”, “pollLast” returns the element and removes it from the deque while “peekLast” only returns the last element.

To learn more about memory leaks in Java, please refer to the Toptal article that demystified this problem.

Common Mistake #5: Excessive Garbage Allocation

Excessive garbage allocation may happen when the program creates a lot of short-lived objects. The garbage collector works continuously, removing unneeded objects from memory, which impacts applications’ performance in a negative way. One simple example:

String oneMillionHello = "";
for (int i = 0; i < 1000000; i++) {
    oneMillionHello = oneMillionHello + "Hello!";
}
System.out.println(oneMillionHello.substring(0, 6));

In Java, strings are immutable. So, on each iteration a new string is created. To address this we should use a mutable StringBuilder:

StringBuilder oneMillionHelloSB = new StringBuilder();
    for (int i = 0; i < 1000000; i++) {
        oneMillionHelloSB.append("Hello!");
    }
System.out.println(oneMillionHelloSB.toString().substring(0, 6));

While the first version requires quite a bit of time to execute, the version that uses StringBuilder produces a result in a significantly less amount of time.

See Part II for more buggy mistakes.