Why Does == Sometimes Work on Integer Objects in Java?

A photo of some leaves with the title of the article overlaid.

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

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:

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.

Coding Tangents (46 Articles)—Series Navigation

As a lifelong learner and aspiring teacher, I find that not all subjects carry the same weight. As a result, some topics can fall through the cracks due to time constraints or other commitments. Personally, I find these lost artifacts to be quite fun to discuss. That’s why I’ve decided to launch a whole series to do just that. Welcome to Coding Tangents, a collection of articles that tackle the edge case topics of software development.

In this series, I’ll be tackling topics that I feel many of my own students have been curious about but never really got the chance to explore. In many cases, these are subjects that I think deserve more exposure in the classroom. For instance, did you ever receive a formal explanation of access modifiers? How about package management? Version control?

In some cases, students are forced to learn these subjects on their own. Naturally, this forms a breeding ground for misconceptions which are made popular in online forums like Stack Overflow and Reddit. With this series, I’m hoping to get back to the basics where these subjects can be tackled in their entirety.

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Most recently, he earned a PhD in Engineering Education and now works as a Lecturer. In his spare time, Jeremy enjoys spending time with his wife and kid, playing Overwatch 2, Lethal Company, and Baldur's Gate 3, reading manga, watching Penguins hockey, and traveling the world.

Recent Code Posts