You would think I’d seen it all in my teaching career, but a new bug just showed up in one of my labs: one that involves using == to compare Integer objects in Java.
Table of Contents
- Wait I’ve Read This Article Before
- That Doesn’t Work, But This Does
- Do You See It Yet?
- Don’t Believe Me? Try It!
- Why Do Variables Fix the Problem?
- The Whimsical Nature of Teaching
Wait I’ve Read This Article Before
You may recall that I wrote a piece titled Why Does == Sometimes Work on Strings in Java just over three years ago. Well, here’s it’s spiritual successor!
Like last time, the inspiration for this post once again comes from code a student wrote for one of our labs. In fact, I’m just going to share their code with you to see if you can figure out what’s wrong with it:
private static boolean isPalindrome(Sequence<Integer> s) {
assert s != null : "Violation of: s is not null";
boolean result = true;
for (int i = 0; i <= s.length() / 2 - 1 && result; i++) {
result = s.entry(i) == s.entry(s.length() - 1 - i);
}
return result;
}
For the purposes of this exercise, you can just assume any methods called on the Sequence exist and do what you expect (e.g., entry() returns the value at the index). You can also assume the code compiles.
That Doesn’t Work, But This Does
The student approached me about this code because they were able to get it to work by making a minor change to their code. Here’s the solution they ended up with:
private static boolean isPalindrome(Sequence<Integer> s) {
assert s != null : "Violation of: s is not null";
boolean result = true;
for (int i = 0; i <= s.length() / 2 - 1 && result; i++) {
int num1 = s.entry(i);
int num2 = s.entry(s.length() - 1 - i);
result = num1 == num2;
}
return result;
}
I stared at both solutions for a good couple of minutes and couldn’t quite come up with how the two solutions were different. I then asked the student to send me the solution, so I could investigate further.
Do You See It Yet?
As it turns out, the bug is really subtle. Though, if the title of this article is any hint, then you probably already see the issue. It has to do with “double equals” or the == operator.
Most folks know that you shouldn’t really use double equals when you’re working with objects, but there is at least one time where you might want to use it: it’s great for checking identity. Want to know if two objects are aliases? Use ==.
I also assumed—perhaps naively—that double equals works with the wrapper types (e.g., Integer, Character, etc.). After all, I assumed that there was this autoboxing/unboxing magic that happened with the wrapper types, meaning they were functionally just the primitive types with extra functionality.
That is not the case. The wrapper classes actually behave just like objects because they are objects. If you compare two Integer objects with the same value using double equals, you will get a return value of false—most of the time.
See, like the String class, the Integer class behaves weirdly. It turns out that Java caches some of the Integers in memory, similar to the string interning process we talked about in the last article. However, unlike string interning, the list of numbers that the Integer class caches is known. It’s the first byte of integers (i.e., -128 to 127).
Don’t Believe Me? Try It!
If you load up a tool like jshell, you can quickly create a few Integer objects as follows:
jshell> Integer x = 500 x ==> 500 jshell> Integer y = 500 y ==> 500 jshell> x == y $4 ==> false
As you can see, when the two integers are outside of the byte range, the comparison comes back false. Likewise, when the two integers are inside the byte range, the comparison comes back true:
jshell> Integer x = 100 x ==> 100 jshell> Integer y = 100 y ==> 100 jshell> x == y $7 ==> true
Now, there is some messiness I didn’t bother to explore deeper. For example, you can apparently configure the JVM to cache a wider range of integers by changing the -XX:AutoBoxCacheMax=<size> option.
Why Do Variables Fix the Problem?
Assuming the caching idea makes sense, you might now be wondering why breaking the line up into variables fixes the problem. Well, it’s because you’re implicitly casting the Integer objects back into primitive integers, and primitive integers can correctly be compared with double equals.
If you don’t like that solution, an alternative solution uses the proper method for comparing objects: equals(). In other words, you can rewrite the broken code as follows:
private static boolean isPalindrome(Sequence<Integer> s) {
assert s != null : "Violation of: s is not null";
boolean result = true;
for (int i = 0; i <= s.length() / 2 - 1 && result; i++) {
result = s.entry(i).equals(s.entry(s.length() - 1 - i));
}
return result;
}
Now, no matter what value the Integer object stores, you will get a correct comparison:
jshell> Integer x = 500 x ==> 500 jshell> Integer y = 500 y ==> 500 jshell> x == y $3 ==> false jshell> x.equals(y) $4 ==> true
The Whimsical Nature of Teaching
One of the reasons I love teaching software development to students is because they write really interesting bugs. So many of the articles in this series are the result of some code I saw a student write, and I expect to write many more of these articles in the future.
In fact, you may recall that I wrote an article titled, The Behavior of i=i++ in Java. I wrote that article all the way back in 2019. Would you believe me if I said I saw another student write the same expression the other day in lab? I could not for the life of me figure out why they had an infinite loop. Then, I audibly chuckled when I finally spotted this expression again.
Anyway, I hope this was an interesting article for you. I swear I learn something new from my students every day. Perhaps I can pass those moments along, such as the ones below, to you:
- Why Is Adding Two Random Numbers Not the Same as Generating One in the Same Range?
- Stop Using Flags to Control Your Loops
- The Remainder Operator Works on Doubles in Java
As always, you can take your support a step further by checking out a few ways to grow the site. If not, no worries. I’ll still be here when you come back.
Recent Code Posts
Contrary to my piece from a couple years ago, I'm actually coming out in favor of incorporating loop invariants in computer science education—albeit with more of a focus on reasoning than formal...
One day I had this dream to start a series of articles going over some of the more obscure and perhaps even harmful features of programming languages. To kick this series off, I wanted to talk about...