The use pit of @ Data in Lombok

Keywords: Programming Lombok Java Attribute REST

When you're using Lombok's @Data When commenting, there will be some holes that need attention. Let's see today.

Lombok

Let's briefly introduce Lombok. Its official introduction is as follows:

Project Lombok makes java a spicier language by adding 'handlers' that know how to build and compile simple, boilerplate-free, not-quite-java code.

Lombok can make Java code simple and fast by adding some "handlers".

Lombok provides a series of annotations to help us simplify our code, such as:

Annotation name function @ Setter automatically adds all attribute related set methods in the class @Getter Automatically add all property related get methods in the class @Builder It enables the class to build the object @ RequiredArgsConstructor through Builder (builder mode) to generate a construction method of the class. It is forbidden to override the toString() method @ EqualsAndHashCode of the class by nonparametric construction @ ToString and override the equals() and hashCode() methods @ Data of the class, which are equivalent to the above @ Setter, @ Getter, @ RequiredArgsConstructor, @ ToString, @ EqualsAndHashCode

It seems that these annotations are very normal and optimize our code to some extent. Why do we say that @ Data annotation has a hole?

@Data annotation

Internal implementation

From the above table, we can see that @ Data contains the function of @ EqualsAndHashCode, so how does it override the methods of equals() and hashCode()?

We define a class TestA:

@Data
public class TestA {

    String oldName;
}

We decompile the compiled class file:

public class TestA {

    String oldName;

    public TestA() {
    }

    public String getOldName() {
        return this.oldName;
    }

    public void setOldName(String oldName) {
        this.oldName = oldName;
    }

    public boolean equals(Object o) {
        // Judge whether it is the same object
        if (o == this) {
            return true;
        }
        // Determine whether it is the same class
        else if (!(o instanceof TestA)) {
            return false;
        } else {
            TestA other = (TestA) o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                // Compare properties in the class (note that only properties in the current class are compared here)
                Object this$oldName = this.getOldName();
                Object other$oldName = other.getOldName();
                if (this$oldName == null) {
                    if (other$oldName != null) {
                        return false;
                    }
                } else if (!this$oldName.equals(other$oldName)) {
                    return false;
                }

                return true;
            }
        }
    }

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

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $oldName = this.getOldName();
        int result = result * 59 + ($oldName == null ? 43 : $oldName.hashCode());
        return result;
    }

    public String toString() {
        return "TestA(oldName=" + this.getOldName() + ")";
    }
}

For its equals() method, when it performs attribute comparison, it only compares the attributes in the current class. If you don't believe it, let's create another class, TestB, which is a subclass of TestA:

@Data
public class TestB extends TestA {

    private String name;

    private int age;
}

We decompile the compiled class file:

public class TestB extends TestA {

    private String name;

    private int age;

    public TestB() {
    }

    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof TestB)) {
            return false;
        } else {
            TestB other = (TestB)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                // Note that here, it really only compares the attributes in the current class, not the attributes in the parent class
                Object this$name = this.getName();
                Object other$name = other.getName();
                if (this$name == null) {
                    if (other$name == null) {
                        return this.getAge() == other.getAge();
                    }
                } else if (this$name.equals(other$name)) {
                    return this.getAge() == other.getAge();
                }

                return false;
            }
        }
    }

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

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $name = this.getName();
        int result = result * 59 + ($name == null ? 43 : $name.hashCode());
        result = result * 59 + this.getAge();
        return result;
    }

    public String toString() {
        return "TestB(name=" + this.getName() + ", age=" + this.getAge() + ")";
    }
}

According to the code understanding, if two subclass objects have the same properties in their subclasses and different properties in their parents, they will still be considered the same when using the equals() method. Test this:

public static void main(String[] args) {
        TestB t1 = new TestB();
        TestB t2 = new TestB();

        t1.setOldName("123");
        t2.setOldName("12345");

        String name = "1";
        t1.name = name;
        t2.name = name;

        int age = 1;
        t1.age = age;
        t2.age = age;

        System.out.println(t1.equals(t2));
        System.out.println(t2.equals(t1));
        System.out.println(t1.hashCode());
        System.out.println(t2.hashCode());
        System.out.println(t1 == t2);
        System.out.println(Objects.equals(t1, t2));
    }

The result is:

true
true
6373
6373
false
true

Summary of problems

For the parent class, Object is used @Class annotated by EqualsAndHashCode(callSuper = true), generated by Lombok The equals() method returns true only when two objects are the same object, otherwise it will always be false, regardless of whether their properties are the same or not.
Most of the time, this behavior is not as expected, equals() has lost its meaning. Even if we expect equals() works like this, so the rest of the attribute comparison code is cumbersome, which will greatly reduce the branch coverage of the code.

Solution

  1. If @ Data is used, there should be no inheritance relationship. It is similar to Kotlin.
  2. Overriding equals(), Lombok does not generate explicitly overridden methods.
  3. Use @ EqualsAndHashCode(callSuper = true) explicitly, and Lombok will be subject to the one explicitly specified.

Posted by jaytux on Sat, 02 Nov 2019 03:21:15 -0700