Recently I’ve had to jump onboard a Java Spring Boot project. Not having used Java for a long time, I was surprised that my code was throwing null pointer exceptions (NPEs).
In C#, I rarely encountered an NPE. Indeed, I thought the new compiler work on null-reference types was a waste of focus. The only time I typically experience these kinds of errors are in scenarios where the compiler can’t help. For example, a thread calls back to an Android Activity which has been destroyed because the user rotated the screen.
What was tripping me up was the difference between Long and long, and related nuances. Java has wrapper classes for primitives. So for the primitive long, there is a class called Long. Mixing the two can be perilous. I’ll focus on Long and long, ignoring the other primitives and their wrappers.
The context of this post is a Spring Boot Application, and it contains my musings on this issue. I’d love to hear from the Java guys out there. Any advice would be very much appreciated.
Here Be Dragons
Firstly, in the case of objects (instances of the Long wrapper class) if you do this:
The result is true.
Whereas, surprisingly at first glance, this is false:
The reason this is false is because the JVM provides Long references pre-defined for the first 30 or so numbers. So in the first example, a and b are the same reference. In the second, they are not.
The next comparison, between a Long and a long, uses auto-unboxing, so this is true:
And this is also true, again because Java unboxes the object to a primitive before comparison:
But auto-unboxing results in an NPE if your object is null, for example if a is null:
Likewise, this also throws an NPE if countryCode is null:
Primitives are values, so comparing them with == compares their values. This is known as value equality. Since objects are references, == compares whether the two references are equal, not their values. This is known as reference equality. Comparing a primitive with an object using == results in Java trying to convert the object to a primitive automatically via a process known as unboxing in order to do a value equality comparison. However, If the object is null, unboxing causes an NPE.
Conceptually comparing Long and long to zero is the same thing, technically it isn’t. I believe it’s this conceptual similarity between primitives and their wrapper classes (i.e. long, Long) which so easily trips us up. They have completely different semantics, yet interface to a degree.
Prefer Value Equality (for two objects)
The first way to improve this situation if you have two objects and you want to know if their values are equal, is to use value equality rather than reference equality.
This example is safe, and behaves as expected:
Check for Nulls
As per the previous example, remember to check if your object is null before invoking a method on it. We see here, that if a is not null, a.equals(d) returns false – no NPE even though d is null.
Be Careful of Literals and Auto-Boxing
When a method takes an Object as its parameter, like equals, Java auto-boxes a literal into an object representing that literal. Boxing is the opposite of unboxing. So an int (0) is represented by an Integer, a long (0L) by a Long, etc. The following code will return false, because the line countryCode.equals(0) is comparing two different types, a Long and an Integer:
If the literal is expressed as a long (0L) the result would be true as we would now be comparing a Long with a Long:
So even when using equals, or auto-boxing, you have to be precise.
This is true, because both are primitives:
The int is implicitly promoted to a long for safe comparison. This can be seen in assignments, as in the previous example where we assigned an int (65) to a long (DEFAULT_COUNTRY_CODE). To avoid implicit promotion, the assigned value should have been written as 65L.
Prefer Constants to Literals (where appropriate)
As mentioned, Java implicitly promotes a primitive value when required, so even if we had used 0 rather than 0L in this example, we would still have the correct type: long (0L). Java will correctly autobox a long to a Long. This prints “no stock” if stockCount is null or 0L, as expected:
Auto-Unboxing and Implicit Promotion
This next example works as expected because stockCount is auto-unboxed to a long primitive, the int 0 is then implicitly promoted to a long (0L), and we have a guard condition ensuring stockCount is not null (via boolean short circuit logic).
This is the best idiom for comparing an object with a primitive (IMO):
If you already know stockCount can’t be null – through a validation framework or a prior check, you can just simply compare the object with the primitive.
A general recommendation: using the correct literal (0L) or primitive variable type (long) is always better than relying on implicit promotion.
It is possible to use casting to ensure correct boxing/unboxing. However, if a is null you will get the dreaded NPE, but if a is a primitive this works fine. This is more useful when passing equals() a variable of a different primitive type such as an int:
Quick Recap on Comparisons
So when comparing:
- an object with a literal, the above mentioned idiom is more appropriate (==)
- two objects, use equals() for value comparison
Always keep in mind:
- ensure the object is not null in a mix comparison (==)
- ensure the lhs object is not null in the case of equals
- Cast primitive type variables in a call to equals where appropriate
- Be precise with literals 0L for long, 0 for int (0 will autobox as an Integer)
- Remember Long.equals(Integer) is false, even if both have the same value
- Long.equals(null) is false, but won’t throw an NPE
- Prefer constants to literals where appropriate, their values are typed correctly
- Don’t assume objects (i.e. Long) returned from method calls are not null
And remember, primitives and their wrappers classes (i.e. long, Long) have completely different semantics. They are not the same thing. They interface to a degree, and that is the cause of most of the issues. Treat them as different things even though conceptually they are similar.
Assignment has the same Problem
Assigning a Long that is null to a primitive variable has the same problem, auto-unboxing will throw an NPE.
Same potential issue with method calls:
Avoid Primitives (for fields and related methods and their parameters)
This is why in Sping Boot I generally prefer to keep the fields in DTOs, Entities, DAOs, etc. of the same type (Primitive wrappers like Long). If not, when adapting between Long and long, Integer and int and so on, I have to keep safe-guarding assignments and comparisons. It’s easy to make a mistake, especially when tired, or habitually coding in multiple languages.
You have to be vigilant, and consistency is the first line of defense.
This is somewhat controversial, especially since it is contrary to Joshua Bloch’s advice in Effective Java Third Edition, Item 61. Joshua is right of course. You typically want to keep your primitive wrapper classes (i.e. Long) on the perimiter of your application, and internally use primitives (long) only. This is much more efficient, it is easier to understand the code base, and you avoid a whole bunch of potential pitfalls.
However, In the context of a Spring Boot application, I find it saner to give up some of the efficiency in order to avoid mixing primitives and their wrapper classes as much as possible.
Of course, I would still use primitives for indexing, number crunching, in functions and encapsulated cases etc. I’m referring to fields, and related methods and their parameters – in DAOs, Entities, Repositories and Services right through to Controllers, Response Entities, and DTOs.
Typically when using a powerful framework you want to leverage it as much as possible. Spring Boot and its eco-system make good use of generics in many places. Primitives cannot be used with generics. So inevitably you will have plenty of Long objects in your code base, they are pervasive.
I understand that not all Spring Boot projects are the same, some are CPU intensive number crunching applications, others have large complex domains and business logic, and many wrap legacy backend systems, in which case avoiding primitives may simply not be an option.
Anyways, I hope the exploration of the issues may have been useful.
Appreciate any feedback and suggestions.