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)
}
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.
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?
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
I imagine Int? is creating a “wrapped Int object” so it’s boxed into an instance of a type then and there.
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
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.