Why does hamcrest say that a byte 0 is not equal to an int 0?
Asked Answered
B

2

22

Consider the following test case using standard JUnit asserts and hamcrest's assertThat:

byte b = 0;
int i = 0;

assertEquals(b, i); // success
assertThat(b, equalTo(i)); // java.lang.AssertionError: Expected: <0> but: was <0>

if (b == i) {
    fail(); // test fails, so b == i is true for the JVM
}

Why is that so? The values are apparently equal for the JVM because b == i is true, so why does hamcrest fail?

Bouffant answered 26/11, 2015 at 15:6 Comment(2)
Because Byte.valueOf((byte) 0).equals(Integer.valueOf(0)) is false.Berni
As seen in assylias' example above, the byte gets auto-boxed into a Byte-object. As seen in the Hamcrest's equalTo docs it uses the Object1.equals(Object2). Since both the byte and int are primitives, it auto-boxes them to Byte and Integer objects. Byte1.equals(Integer1) will return false, even though the values of these boxed object are the same.Guidance
R
29

Assert#assertThat is a generic method. Primitive types don't work with generics. In this case, the byte and int are boxed to Byte and Integer, respectively.

It then becomes (within assertThat)

Byte b = 0;
Integer i = 0;

b.equals(i);

Byte#equals(Object)'s implementation checks if the argument is of type Byte, returning false immediately if it isn't.

On the other hand, assertEquals is Assert#assertEquals(long, long) in which case both the byte and int arguments are promoted to long values. Internally, this uses == on two primitive long values which are equal.


Note that this boxing conversion works because assertThat is declared as

public static <T> void assertThat(T actual, Matcher<? super T> matcher) {

where the byte is boxed to a Byte for T, and the int is a boxed to an Integer (within the call to equalTo), but inferred as a Number to match the Matcher<? super T>.

This works with Java 8's improved generic inference. You'd need explicit type arguments to make it work in Java 7.

Remora answered 26/11, 2015 at 15:10 Comment(0)
S
13

This happens because the int and byte are boxed to Integer and Byte as hamcrest matchers operate on objects, not on primitives. So you are comparing an Integer with a Byte, and the implementation of Byte.equals() is:

public boolean equals(Object obj) {
    if (obj instanceof Byte) {
        return value == ((Byte)obj).byteValue();
    }
    return false;
}

and Integer.equals():

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

In other words, an Integer and Byte are always unequal. When comparing primitives, just use Assert.assertEquals instead. The hamcrest matchers are powerful, but mostly intended for (complex) object assertions.

Sergent answered 26/11, 2015 at 15:10 Comment(3)
Is there any reason for Java to not check for values in the same range? E.g. if (obj instanceof Integer) { return ((Integer)obj).intValue() == (int) value;} in Byte.equals()?Bouffant
@Bouffant Well, that's probably the reason we have primitives we can use instead. When Java compares two objects, it first checks if both objects are of the same type; if not, it simply returns false. Integer and Byte are objects, so the same applies to them. If (new Byte(0)).equals(new Integer(0)) would return true it would be a bit strange to me. A comparable example would be if (new Dog("Luke")).equals(new Cat("Luke")) would return true, simply because they have the same name (I know, not the best example here, but then you see how strange it looks if it would return true).Guidance
@KevinCruijssen It is a thing that you don't expect when boxing happens. One could argue that Number could require an equals that does allow Integer and Byte to be comparable, but that would probably again lead to unexpected behavior with Float and Double or very complex implementation to take into account 'other' number types.Sergent

© 2022 - 2024 — McMap. All rights reserved.