The Inheritance & HashSet Related Bug with Lombok

The Rookie Leader
Vena Engineering
Published in
5 min readNov 5, 2019

FUN FACT: Lombok is an Indonesian island east of Bali and west of Sumbawa, part of the Lesser Sunda Island chain. That’s a picture of the island above.

While testing an existing piece of code that used Jackson’s Object Mapper to parse a JSON String into a HashMap that had Integers as keys, and HashSet as values. The JSON Object had about 20,000 entries but the Set was returning just one entry. I initially thought it was a Jackson issue, and after debugging, came to the following conclusions.

Here we have the defined Parent class:

package org.picture.example;import lombok.Data;@Data
public class Parent {
private String name;
private int age;
}

Here we have the define Child class:

package org.picture.example;import lombok.Data;@Data
public class Child extends Parent {
}

Next, we have a method that parses a JSON object to a Map<Integer, Set> collection:

package org.picture.example;import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JavaType;
public class Mapper {public Map<Integer, Set> jsonToMap(String json) {
ObjectMapper mapper = new ObjectMapper();
Map<Integer, Set<T>> convertMapping;
try {
JavaType innerType = mapper.getTypeFactory().constructCollectionType(Set.class, Child.class);
JavaType integerType = mapper.getTypeFactory().constructType(Integer.class);
convertMapping = mapper.readValue(json, mapper.getTypeFactory().constructMapType(Map.class, integerType, innerType));
} catch (IOException ex) {
}
return convertMapping;
}}

THE DEFINED CLASSES

We had defined a class which was inheriting from a parent class. This particular child class did not define its own fields but was using the fields defined in its parent class. You might wonder why we needed the child class; it was to take into consideration the possibility of that child class having to define its own unique fields in the future. Also, because there were other child classes inheriting from this parent class.

INVESTIGATING

The initial bug was related to the fact that we were parsing a JSON Object which contained about 20,000 entries, but when parsed using Jackson’s ObjectMapper, we only got a Set with one entry. Further investigation led to the key problem.

LOMBOK AND EMPTY CLASSES (EQUALS & HASHCODE METHODS)

When you define an empty class, regardless of whether it extends a parent class or not, if you annotate it with @Data or @EqualsAndHashCode, or any annotation that generates a hashCode() method, the method will always return 1. Weirdly, once you define a field in that class, then Lombok actually creates a ‘proper’ hashCode() method, which calculates the hashCode based on a few values and conditions, which ensures uniqueness. Lombok seems to completely ignore the parent class, or maybe at the time, it is not yet aware of the parent class. Based on information online, it appears to be a known fact that you specifically have to let Lombok know if there is an existing parent class, using arguments defined in annotations, or specific annotations/methods.

In the case of the equals() method, since there are no defined fields and Lombok isn’t aware of the parent class, the equals() method only checks if the objects are of the same instance.

public boolean equals(final Object o) {
if (o == this) return true;
if (!(o instanceof Child)) return false;
final Child other = (Child) o;
if (!other.canEqual((Object) this)) return false;
return true;
}

protected boolean canEqual(final Object other) {
return other instanceof Child;
}

public int hashCode() {
int result = 1;
return result;
}

In order to debug this, you need to have access to the methods generated by Lombok. Lombok’s IntelliJ plugin provides a Delombok function.

HOW TO DELOMBOK ON INTELLIJ

  1. Install the Lombok plugin for IntelliJ if you don’t have it already.
  2. Restart IntelliJ.
  3. Define a class and annotate it with @Data or @EqualsAndHashCode.
  4. Right click and select Refactor, select Delombok and the annotation you want to investigated. It will generate all the methods defined within that annotation. You’ll see that the hashCode() method always returns 1.

IMPLICATION WITH HASHSET

In a case where you are using a HashSet to collect your data, you will always end up with a HashSet with only one entry. Here’s why: a major characteristic of HashSets is that every value entered into the HashSet has to be unique. In our scenario, we are inserting several Child objects. What Java does when inserting into a HashSet is to check each entry if it has the same hashCode or is equal to an existing entry in the Set. So it goes to the hashCode() method that Lombok generates and compares the hashCode values. With Lombok defining hashCode methods to return 1, all objects will always have the hashCode value of 1. We face a similar issue with the equals() methods; when adding an entry to a HashSet, for a child class with no defined fields, Lombok’s equals() method basically only checks if they are of the same instance, which will always return true. Therefore, every added object will always be equal to the initial one, so you end up with one entry inserted into your HashSet.

One question I had was, why was the object still being returned with its parent’s fields? Why did we not have an empty object? The answer, if we put Lombok aside, the compiler is able to determine at compile time that the object has a parent with defined fields. The Java complier still works great, the problem was just with Lombok.

FIXING THE BUG

After the investigation, I found that there are several ways to fix the bug. I’ll list them from the most technically ideal to least ideal:

  • On the child class, use the @EqualsAndHashCode annotation and set the callSuper parameter to true. This then ensures that the parent’s equals() and hashCode() methods are called for the child class.
package org.picture.example;import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class Child {
}
  • Remove the @Data annotation on the child class, the compiler and Lombok work better together that way. In this case, this Java compiler will be able to determine it’s a child class inheriting from the parent and Lombok will generate all the methods for the parent class.
  • If possible, avoid defining a child class if you don’t absolutely have to. If you’re certain you will never have to define unique fields and behaviours for that class in the future, then by all means, don’t define one.
  • Last one, which is the least ideal, don’t use a HashSet. Using ArrayLists will easily solve the problem, or put a plaster over it. You probably won’t even be aware that the bug exists, until you maybe know for sure that you are entering 10 entries but somehow end up with more because ArrayLists don’t enforce uniqueness.

--

--

The Rookie Leader
Vena Engineering

Software and DevOps Engineer turned Engineering Leader, sharing my perspective on what leadership in tech looks like and should look like.