3 minute read ◦ posted on 23 Aug 2022 by Hanno Embregts .

In the summer of 2021, I got my Java 11 certification. I expected it to be quite a breeze, because I’d been a Java developer for 14 years and surely I should have seen it all by now, right? Turned out I was very wrong. I came across lots of things that I didn’t even know were possible with Java. In this weekly blog series I will go through 11 of these ‘crazy learnings’ that surprised me the most, even as an experienced developer. Today is about equality when you’re dealing with wrapper objects.

Wrapper objects are immutable

When we compare primitives, we are used to using the == operator to test equality. However, when we start using wrapper objects instead of primitives, we have to switch to using invocations of the equals() method to test equality. The reason for this is that all wrapper objects are immutable.

For example, when you run the following code…

Integer i = 200;
i++;

…what actually happens is something like this:

Integer i = 200;
i = Integer.valueOf(i.intValue() + 1);

As you can see, a different Integer object is assigned back to i, because of immutability.

Different identity

Because the wrapper objects are immutable, two separate objects containing the same value will not be equal to each other (at least not according to the == operator). So although two Integer objects might be similar because of the values they hold, they’ll still be different based on their identity.

Let’s try this out using a code example. We’ve defined a static method that applies the == operator to some Integer parameters…

public static boolean areIntegersEqual(Integer a, Integer b) {
    return a == b;
}

…and we’ll run the following test method to assert that two similar Integers are not equal at all:

@Test
@DisplayName("integerEquals() should return false when the same arguments (200) are passed")
void integerEqualsShouldReturnFalseWith200() {
    assertThat(areIntegersEqual(200, 200)).isFalse();
}

Wrapper object caching

Well, if two Integers that hold the value 200 are not equal to each other, then surely the same will be the case when we compare two Integers with the value 10, right?

@Test
@DisplayName("integerEquals() should return false when the same arguments (10) are passed")
void integerEqualsShouldReturnFalseWith10() {
    assertThat(areIntegersEqual(10, 10)).isFalse();
}
org.opentest4j.AssertionFailedError: 
Expecting value to be false but was true
Expected :false
Actual   :true

How peculiar! There must be a good reason for this behaviour. Well, of course there is - there always is!

It turns out that, to save on memory, Java ‘reuses’ all the wrapper objects whose values fall in the following ranges:

  • All Boolean values (true and false)
  • All Byte values
  • All Character values from \u0000 to \u007f (i.e. 0 to 127 in decimal)
  • All Short and Integer values from -128 to 127

This means that the == operator will always return true when their primitive values are the same and belong to the above list of values.

Java Language Specification

This is what the Java Language Specification has to say on the subject:

If the value p being boxed is the result of evaluating a constant expression (§15.28) of type boolean, char, short, int, or long, and the result is true, false, a character in the range ‘\u0000’ to ‘\u007f’ inclusive, or an integer in the range -128 to 127 inclusive, then let a and b be the results of any two boxing conversions of p. It is always the case that a == b.

(from the Java 11 Language Specification, paragraph 5.1.7)

In conclusion: some wrapper objects are more equal than others! And now you also know why and when.

Confused about maths

Photo by Karolina Grabowska from Pexels

Other blog posts in this series

Did you miss a blog post in this series? Here’s a list of all posts that have been published so far:

  1. A few freaky array declarations
  2. Stream elements should implement Comparable
  3. Accessing static interface methods
  4. Anonymous subclasses in enums
  5. Division by zero
  6. Method overloading priorities
  7. The crazy stuff that is allowed in switch statements
  8. Equality in cloned arrays
  9. Wrapper objects: some are more equal than others
  10. Functional interfaces actually CAN contain multiple abstract methods
  11. Passing arguments to method references