Equality not working as expected with Kotlin code testing Java class?


#1

Hi all, I converted several test classes to Kotlin and now have a test failing.

This is a pared down version of the class being tested. The issue is when kotlin tests this class it is unable to find the item in the find method whereas the Java variant is.

public class ObjectTracker {
    private final List<Object> mList = new ArrayList<>();

    public void add(Object object) {
        mList.add(object);
    }

    public Object find(Object value) {
        for (Object listItem : mList) {
            if (listItem == value) {
                return listItem;
            }
        }

        return null;
    }
}

Below is the original code and the code that resulted from the “Convert to Kotlin” action. Both test cases pass with the original Java code. With the Kotlin code only the “test string” test passes:

@Test
public void testAddingAndFindingInteger() {
    ObjectTracker objectTracker = new ObjectTracker();
    Integer value = 1000;
    objectTracker.add(value);

    Object foundValue = objectTracker.find(value);
    assertTrue(foundValue == value);
}

@Test
public void testAddingAndFindingString() {
    ObjectTracker objectTracker = new ObjectTracker();
    String value = "test";
    objectTracker.add(value);

    Object foundValue = objectTracker.find(value);
    assertTrue(foundValue == value);
}

The converted Kotlin code:

@Test
fun testAddingAndFindingInteger() {
    val objectTracker = ObjectTracker()
    val value = 1000
    objectTracker.add(value)

    val foundValue = objectTracker.find(value) // Does not find the value and so fails the assertion below...
    assertTrue(foundValue === value)
}

@Test
fun testAddingAndFindingString() {
    val objectTracker = ObjectTracker()
    val value = "test"
    objectTracker.add(value)

    val foundValue = objectTracker.find(value) // Finds the value and passes the assertion below...
    assertTrue(foundValue === value)
}

#2

The difference in the behavior is caused by boxing. In your Java code, you’re manually creating an instance of java.lang.Integer and passing it to the add method and to the find method. In your Kotlin code, val value = 1000 defines a variable of type Int, which corresponds to Java’s int. When you call the add method, the int will be boxed into an instance of java.lang.Integer, and when you call find(), it will be boxed again so that a new instance will be created. Your code compares object identity, and therefore will not find the instance.


#3

Probably Kotlin-to-Java converter should preserve java.lang.Integer type instead of replacing it with Int.


#4

per vbezhenar’s comment below, what would the solution be to get the Kotlin code to work without modifying the original Java?

Probably Kotlin-to-Java converter should preserve java.lang.Integer type instead of replacing it with Int.

I can’t use this syntax in Kotlin to preserve the java.lang.Integer type:

    val i1: java.lang.Integer = 1000

#5

What do you mean by “without modifying the original Java”?

You can use the following declaration: val i1: Int? = 1000


#6

What do you mean by “without modifying the original Java”?

Sorry I should have specified that in tinkering around a bit I had changed the equality check from if (listItem == value) to if (listItem.equals(value)) to experiment and the tests passed. This makes sense given your comment about checking object identity. But I didn’t want to modify this original code, I was just trying it out b/c I was confused about the issue.

Thanks for the suggestion on adding Int? as the type, that made the test pass. If you have any extra time could you give me a brief lesson on why this fixes the auto-boxing issue you mentioned?


#7

I think that you force the compiler to create a wrapped Int when declaring a nullable type (since primitive types cannot be null).


#8

Yup. The tests pass so it seems adding Int? was the solution, this makes sense because the find method can return null. But at the same time the auto-boxing aspect of this was completely unintuitive to me. That’s why I’ve asked for a quick primer on why that’s the case. @yole appreciate the help :slight_smile:

I imagine Int? is creating a “wrapped Int object” so it’s boxed into an instance of a type then and there.


#9

A local variable of type Int? will have the type java.lang.Integer in the bytecode, so it will behave exactly as your original Java test.


#10

@yole

Imagine I had a method that didn’t return null and I didn’t need the Int? type for that reason, but I still wanted to perform some instance check in the method or I wanted to check referential identity in some way. Would I then have to make it an Int? type anyway in order to have an instance of Integer?

So in my code I’d have something like:

val i1 = 1000 // I can't have a reference to this var...
val i2 : Int? = 1000 // I want a reference so I'll use Int?, but realistically it will never be null

Is this a realistic use case?


#11

Sorry, I can’t imagine any real-life code that would need to perform identity checks on boxed integers. If you have a real, non-artifical use case, I’d be happy to discuss the best way to represent it in Kotlin.


#12

No worries, I was just imagining and didn’t have any real example. Thanks for your help!