What mistakes should be avoided while programming in Java? In the following piece we answers this question.
Java is a popular language with an established position in the world of software development. It is a strong and versatile programming language. Around 3 billion devices worldwide run on Java and, therefore, at least 3 billion mistakes were made when using it. In this article, let’s focus on how to not make any more.
1. Getting Concurrent Modification Exception
This is by far the most common mistake I came across. In the early days of my career, I made it many times too. This mistake occurs when you try to modify the collection while you iterate through it. The ConcurrentModificationException may also be raised when you work with multiple threads but for now, let’s focus on a base scenario.
Assume you have a Collection of users where some of them are adults and some are not. Your task is to filter out the children.
for (User : users) {
if (!user.isAdult()) {
users.remove(user);
}
}
Running the aforementioned code ends in getting ConcurrentModificationException. Where did we go wrong? Before finishing our iteration, we tried to remove some elements. That’s what triggers the exception.
How can I avoid it?
There is a couple of approaches that can help in that case. First and foremost, take advantage of Java 8’s goodness – Stream.
Using a Predicate filter, we’ve done the inverse of the previous condition – now we determine elements to include. The advantage of such an approach is that it’s easy to chain other functions after the removal, e.g. map. But for goodness’ sake. please do not try to do something like below:
It could also end up in the ConcurrentModificationException because you are modifying the stream source. It can also give you some more Exceptions which will not be easy to debug.
To solve ConcurrentModificationException in a single-thread scenario. you could also switch to using directly Iterator and its remove() method, or you could simply not remove elements during iteration. However, my recommendation is to use Streams – it’s 2022.
2. Storing passwords as Strings
As I am getting more and more involved with cybersec, I wouldn’t be true to myself if I didn’t mention at least one Java mistake that can lead to a security issue. Storing passwords received from users in a String object is exactly something you should be afraid of.
The issue (or maybe advantage) of String is that it is immutable. In the cyberseced world, it creates a potential threat since you cannot clear the value of a once create String object. The attacker who gets access to your computer’s memory can find plain text passwords there.
Secondly, strings in Java are interned by the JVM and stored in the PermGen space or in the heap space. When you create a String object, it gets cached, and it is only removed when the Garbage Collector starts doing its job. You cannot be sure when your password is deleted from the String pool since the Garbage Collector works in a non-deterministic way.
How to avoid it?
The recommended approach is to use char[] or, even better, the library that supports storing passwords as char[], e.g.Password4j. The char[] array is mutable and can be modified after it has been initialized. After processing a password, you can just erase the char[] password array by writing random chars in it. In case the attackers get access to your computer’s memory, they will only see some random values that have nothing to do with users’ passwords.
3. (Un)handling Exceptions
Newbies and also more advanced programmers don’t know how to handle exceptions correctly. Their main sin in that matter is just ignoring them. IT IS NEVER A GOOD APPROACH.
Unfortunately, we cannot give you a silver bullet solution that will fit into every Exceptions’ scenario you come across. You have to think about each case separately. However, we can give you some advice on how to get started on that topic.
How can I avoid it?
Ignoring Exceptions is never a good practice. Exceptions are thrown in for some reason, so you should not ignore them.
try {...} catch(Exception e) { log(e); } is rarely the correct approach to Exception handling.
Rethrow Exception, show an error dialog to the user or at least add a comprehensive message to the log.
If you left your exceptions unhandled (which you should not), at least explain yourself in the comment.
4. Using null
Unfortunately, it is quite common to find a Java function that in some cases return a null. The problem is that such a function enforces its client to perform a null check on the result. Without it, the NullPointerException is thrown.
The other thing is passing a null value. Why did you even think of that? In such a case, the function has to perform a null-check. When you use third-party libraries, you cannot change the insides of the functions. What then?
More importantly, other developers that read your code and see that you pass null will probably be disoriented as to why you choose such a bizarre way to implement your feature.
How can I avoid it?
Do not return a null value! Ever! In case your function returns some type of Collection, you can just return an empty Collection. If you deal with single objects, you can make use of the null object design pattern. Since Java 8, it is implemented as Optional. Other than that, the least recommended approach involves raising an Exception.
5. Heavy String concatenation
Hopefully, it is not a mistake you make, since it’s the most popular (or maybe second most popular after FizzBuzz) interview question. As you should know by now, a String object is immutable in Java – once created, it cannot be modified. So concatenation of String literals means a lot of unnecessary memory allocation. Concatenating String objects each time requires creating a temporary StringBuilder object and changing it back to a string. Therefore, this solution is absolutely not suitable if we want to combine a large number of characters.
How can I avoid it?
To solve that issue, use StringBuilder. It creates a mutable object that can be easily manipulated. Of course, you can always use StringBuffer if your project is used in a concurrent context.
6. Not using existing solutions
When developing software, getting to know the basics of the language you write in is a must but is not enough. Many algorithmic problems you came across while implementing a new feature have already been solved by someone else. Too many times I’ve seen someone implement a security algorithm from scratch. Such an approach is error-prone. One person cannot thoroughly test such a complex solution. The collective knowledge of the team that consists of mid-advanced programmers is almost always better than the greatness of one prodigy Java developer. There is no need for you to reinvent the wheel – you just have to adapt the existing solution to suit your needs.
How can I avoid it?
Try to search for libraries that tackle the problem you are working on. Try to find similar solutions. Many of the libraries that are available on the web are free and have been polished and tested by experienced devs and the whole Java community. Don’t be afraid to make use of them.
7. Not finding enough time for writing tests
It’s tempting to believe that our code will always run perfectly. Not writing tests for code is the worst sin of Java software developers. Many of us prefer manual and exploratory tests instead of unit tests, which is bonkers. <joke> Why waste time writing tests when you can focus on providing the world’s best code for your project, that DEFINITELY has no bugs?<joke>. It turns out the reality is brutal and we cannot provide high-quality code without writing tests.
How can I avoid it?
You should always prepare tests for your code. I know the TDD approach is not so easy to maintain but you at least should provide tests that cover all conditions in which your code can be run. This includes testing exceptional situations. The unit tests are necessary. You have to provide them for every feature of your project if you want to make sure your code is easy to refactor and extendable in further development.
One more thing. Maintain a high standard of your test code – it will be worth it. That’s Uncle Bob’s advice and I totally agree with it.
Moreover, do not forget about other types of tests. Integration tests are a thing you should consider in your every project.
8. Forgetting about access modifiers
Private and public, right? How can we forget about them. Turns out there are more. When you first started learning Java, you definitely learned about protected access modifiers. They can be useful in some cases, so it’s worth knowing about their existence.
Java developers often seem to forget about the package scope. It is easy to not remember about using it since it is implicit and does not require any Java keywords. The package scope is important. It lets you test a protected method. Protected items are accessible from the test class path, as long as the package is the same.
How can I avoid it?
Remember about the protected modifier and that the package scope allows you to test it.
9. Using pure JavaEE instead of Spring
The next step after learning Java SE is to learn how to run Java on servers, how to make an enterprise-level application.
Newbies often fall into a trap of learning JavaEE since there is a huge number of tutorials about it. Even ‘Thinking in Java’, the Java programmers‘ bible, mentions JavaEE and says nothing about the other options.
How can I avoid it?
JavaEE is a song of the past. Nowadays, Spring is a go-to thing and Java EE is just nice to have. Every modern enterprise-level application uses Spring, so you should strongly consider learning it here.