Today, after a month of working on a new feature, we were ready to have a bug bash, which essentially saved me before ramping this new feature to the public.
As I began debugging my code, I found myself examining a piece of code that utilizes streams, a crucial tool for functional programming in Java when working with collections.
I appreciate how elegant, concise, and readable the code becomes when using map to transform items, apply filters, find minimum and maximum values. However, this time, my lack of experience using these features worked against me.
So, What happened?
I was working with a piece of code performing some transformations like this:
entities.stream()
.map(Entity::getId)
.map(new EntityCriteria()::setEntity)
.collect(Collectors.toList())
If you're more experienced than I was when I wrote this code, you might have already spotted the issue. Allow me to explain:
-
entitiesis a collection (List) and its elements are type ofEntity -
map(Entity::getId)will transform the elements into their IDs - The intention of map(new EntityCriteria()::setEntity) was to transform the entity IDs into EntityCriteria objects with the entityId. However, this happened, but not in the right way.
Understanding the Concepts.
To comprehend what is wrong with number 3, we need to grasp some functional programming concepts:
Entity::getIdfrom theEntityclass uses the::operator to retrieve aFunctioncalledgetId. It does NOT retrieve the actual ID.
ThisFunctionwill be applied to theEntityelements to retrieve their IDs becauseEntityis a class, andgetIddoesn't accept any arguments.When we call
map(new EntityCriteria()::setEntity),new EntityCriteria()creates a new object. Then::setEntityon that object generates aFunctioncapable of applyingsetEntityto the newEntityCriteriaobject.
Root Cause
The Function created by new EntityCriteria()::setEntity is always applied to the same EntityCriteria object, It doesn't create new objects and then retrieve the function to apply setEntity.
The Most Important Question: Why this wasn't caught in Unit Tests ?
The mocked data used in the unit tests, replicated the same buggy logic for transforming the entities, so everything appeared to work.
Possible solutions:
Using Lambdas
map(entityId -> new EntityCriteria().setEntity(entityId))
When we use lambdas, we define the function where the entityId on the left is the input to the function, and the right part is the body of the function (In this case, the transformation logic).
Creating a new method to be applied in map
private static EntityCriteria createEntityCriteria(long entityId) {
return new EntityCriteria().setEntity(entityId);
}
...
entities.stream()
.map(Entity::getId)
.map(MyClass::createEntityCriteria)
.collect(Collectors.toList())
This approach creates a Function that applies createEntityCriteria and that function generates a new EntityCriteria object for each element.
Conclusion
- Everybody makes mistakes, so make sure to thoroughly test your code.
- When writing mock data in unit tests, avoid replicating the logic for building the transformation if that is what you are actually testing.
Top comments (2)
nice explanation man good job finding the bug, you can only learn from mistakes :D AIDE!!
EN ?
Thanks man! Debugger helps a lot, seriously, there is where I found out I was sending the same Object N times.