Probably many of you still remember the lack of type checking in Java 1.4 Collections and how much hassle it was to deal with casting the collection elements, not to mention how many errors this introduced to the code. Since introduction of generics in Java 1.5 this have really improved and one might think that nowadays the language itself protects the programmer from the silliest of typing mistakes. Generics themselves brought with them a set of new complications, but it seems reasonable to think that in the basic code situations when using Java’s Sets or Maps and when there is no kung-fu complications like wildcards or casting we are safe and secure. Are we really?
Recently I have encountered a bug in a production code when dealing with a simple (really simple) usage of a Map. The embarassing part of this issue was that it took lots of effort to debug it and the cause of it was… well… trivial. The code below shows a sketch of how the code looked like before the bug was introduced. In real life the class was a part of a really old and rarely edited legacy code, obviously it was much larger than the one below:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: |
import java.util.HashMap;
import java.util.Map;
public class EmployeeDataLookup {
// A map storing relation between employee ID and name.
private Map<Integer, String> employeeIdToName;
public EmployeeDataLookup() {
// Create a new EmployeeDataLookup and initialize
// it with employee names and IDs.
employeeIdToName = new HashMap<Integer, String>();
addEmployee(301, "John Doe");
addEmployee(302, "Mary Poppins");
addEmployee(303, "Andy Stevens");
}
public void addEmployee(int employeeId, String employeeName) {
employeeIdToName.put(employeeId, employeeName);
}
// This class is very complicated and has many other methods...
// Lookup method for finding employee name for given employee ID.
public String findEmployeeName(int employeeId) {
return employeeIdToName.get(employeeId);
}
public static void main(String[] args) {
// Create a EmployeeDataLookup instance
EmployeeDataLookup employeeLookup = new EmployeeDataLookup();
// Find the name of an employee with ID = 301
String employeeName = employeeLookup.findEmployeeName(301);
System.out.print("Employee 301 : " + employeeName);
}
}
|
So what went wrong? Well, at some point the project functionality requirements have changed (surprising, right?) and the key of the map storing data inside of the class had to be changed. In terms of the example above you might say that a company has merged with one of the vendors, so the system storing the employee data had to be made compatible with the ID system of the vendor. Because the vendor company used 13 digit IDs to identify its employees the change to the code in Java terms meant that instead of using Integers we had to change to Longs. Simple right? Its even easier if you use an IDE like Eclipse – just change/refactor the Map key type in line 7 from Integer to Long, let the editor show you all the errors, fix them and that is it! In five minutes all is done:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: |
import java.util.HashMap;
import java.util.Map;
public class EmployeeDataLookup2 {
// A map storing relation between employee ID and name.
private Map<Long, String> employeeIdToName;
public EmployeeDataLookup2() {
// Create a new EmployeeDataLookup and initialize
// it with employee names and IDs.
employeeIdToName = new HashMap<Long, String>();
addEmployee(301, "John Doe");
addEmployee(302, "Mary Poppins");
addEmployee(303, "Andy Stevens");
}
public void addEmployee(long employeeId, String employeeName) {
employeeIdToName.put(employeeId, employeeName);
}
// This class is very complicated and has many other methods...
// Lookup method for finding employee name for given employee ID.
public String findEmployeeName(int employeeId) {
return employeeIdToName.get(employeeId);
}
public static void main(String[] args) {
// Create a EmployeeDataLookup instance
EmployeeDataLookup employeeLookup = new EmployeeDataLookup();
// Find the name of an employee with ID = 301
String employeeName = employeeLookup.findEmployeeName(301);
System.out.print("Employee 301 : " + employeeName);
}
}
|
The change above introduced the bug. Can you see it? When you run the main method now you will find out that the name of the employee with ID=301 is ‘null’! Ha!
If you do not see the trouble (or are to lazy to try) I’ll give you a hint: check out the lines 25 and 26. If you have seen it, try to imagine that this class in real life had 1000+ lines of code unrelated to the changed map. Since there are no compile errors it was like looking for a needle in a haystack… so what’s the bug exactly?
The problem is that after introducing generics, probably due to backward compatibility, some of the methods in Collection and Map interface have not been changed and still do not perform type checking. One of them is Map.get(Object) which have caused the bug in the code above. After the ’simple change’ we have still been using Integer keys to access the map that contained Longs, so even though we had a record of employee 301 in our system the get method returned null. And because of the lack of type checking there were no warnings… Busted.
So what’s the lesson from this? Be cautious about type checking even in the most basic situations. Remember that access methods in Sets, Lists and Maps can behave and bite you in the behind. The biggest pain is that the bugs introduced this way are usually hard to spot by a human, so sometimes a dozen of engineers staring at the code can have trouble finding it. There is a way to deal with them though – most of them can be easily found if you are using a static code analysis tool (eg: FindBugs).
12 Comments until now
In my opinion this is more about autoboxing rather than generics. If I’m not wrong this issue was discussed in one of the talks of Joshua Bloch (?)
IntelliJ IDEA flags this (and hundreds of other problems) as a warning live in the IDE. The static analysis in IDEA has played a huge part in preventing these sorts of bugs for us, I’d feel lost writing code without it these days.
There is some truth in that – autoboxing definitely does not help in figuring where is the bug. But then in the 26th line of the code snippet I could have explicitly created new Integer object so that there is no autoboxing and the issue would still be the same.
Generally I wanted to show that when you access a generic map in line 26 Java does not care about the class of the object you’re using. You could do employeeIdToName.get(new NullPointerException()) and still the code would compile. This is very tricky bug to find and remove.
Having unit tests for the classes would have saved you all the effort.
Thanks for that info Chris! I have to check this out, it would be very helpful!
Yeah, UnitTests rock:)
Generics moronicity.
http://groups.google.com/group/comp.lang.java.programmer/
You might want to try FindBugs, it finds this and a million other bugs.
And it’s free.
Pretty good post. I just came across your blog and wanted to say
that I’ve really enjoyed browsing your blog posts. Anyway
I’ll be subscribing to your feed and I hope you write again soon!
@Shams – DING! I was thinking the same thing! Unit tests should have caught this type of thing and is the very reason to write them! Spend the time up front and make your unit tests meaningful not just so that your code passes code coverage!
[...] one of the first posts on this blog I have discussed the problem of type checking (or lack of it) in some of Java [...]
[...] Even the code in lines 10-13 will not result in a exception! Due to many factors (see posts about map type safety and checked set) the error will wait until line 14 to crash your application. The stack trace you [...]
Add your Comment!