Tags:

@EqualsAndHashCode trong Project Lombok

Trong bài viết này, chúng ta sẽ cùng tìm hiểu @EqualsAndHashCode annotation trong Lombok dùng để generate equals()hashCode() method.

equals()hashCode() có mối tương quan đặc biệt. hoặc là override cả 2 method cùng lúc, hoặc là không.

Để sử dụng Lombok trong project Maven, chúng ta phải thêm dependency:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
</dependency>

@EqualsAndHashCode Class

Khi chúng ta chú thích @EqualsAndHashCode cho 1 class, Lombok sẽ generate equals()hashCode() cho nó. 

import lombok.EqualsAndHashCode;

import java.time.LocalDate;

@EqualsAndHashCode
public class Employee {
    private String name;
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;
}

Mặc định, Lombok sử dụng các thuộc tính non-staticnon-transient để generate equals()hashCode() method – Trong trường hợp của chúng ta là namesalary.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import java.time.LocalDate;

public class Employee {
    private String name;
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;

    public Employee() {
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Employee)) {
            return false;
        } else {
            Employee other = (Employee)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                label43: {
                    Object this$name = this.name;
                    Object other$name = other.name;
                    if (this$name == null) {
                        if (other$name == null) {
                            break label43;
                        }
                    } else if (this$name.equals(other$name)) {
                        break label43;
                    }

                    return false;
                }

                if (this.salary != other.salary) {
                    return false;
                } else {
                    Object this$createdAt = this.createdAt;
                    Object other$createdAt = other.createdAt;
                    if (this$createdAt == null) {
                        if (other$createdAt != null) {
                            return false;
                        }
                    } else if (!this$createdAt.equals(other$createdAt)) {
                        return false;
                    }

                    this.getClass();
                    other.getClass();
                    if (10 != 10) {
                        return false;
                    } else {
                        return true;
                    }
                }
            }
        }
    }

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

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $name = this.name;
        int result = result * 59 + ($name == null ? 43 : $name.hashCode());
        result = result * 59 + this.salary;
        Object $createdAt = this.createdAt;
        result = result * 59 + ($createdAt == null ? 43 : $createdAt.hashCode());
        int var10000 = result * 59;
        this.getClass();
        result = var10000 + 10;
        return result;
    }
}

Trên đây là Employee class được generate sau khi sử dụng @EqualsAndHashCode annotation. Lombok cũng thêm canEqual() method để xác định nhanh object có phải là instance của Employee class hay không.

Ngoài ra thì các bạn để ý rằng các biến final – finalVartransient – transientVar không được Lombok sử dụng trong equals()hashCode() method. 

Exclude Fields

Các thuộc tính định danh (ID), thời gian khởi tạo object etc không nên tham gia vào equals()hashCode() method vì mỗi object được tạo ra thì các giá trị này sẽ khác nhau dẫn đến giá trị hashCode() của mỗi object đều khác nhau, equals() sẽ luôn trả về FALSE.

Exclude Field Level

Sử dụng @EqualsAndHashCode.Exclude chú thích các thuộc tính không được sử dụng khi Lombok generate equals() hashCode() method.

Ví dụ loại bỏ createdAt trong Employee class

import lombok.EqualsAndHashCode;

import java.time.LocalDate;

@EqualsAndHashCode
public class Employee {
    private String name;
    private int salary;
    @EqualsAndHashCode.Exclude
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;
}

Exclude Class Level

Ngoài ra, chúng ta có thể loại bỏ các thuộc tính được generate với thuộc tính exclude trong @EqualsAndHashCode.

Exclude nhận một mảng String chứa tên các thuộc tính cần loại bỏ, nếu tên không trùng khớp với bất kỳ thuộc tính nào của class sẽ được bỏ qua. Các bạn yên tâm, nếu nhập tên không khớp thì các IDE sẽ cảnh báo, nhiệm vụ các bạn là hãy chú ý các thông báo của IDE nhé.

import lombok.EqualsAndHashCode;

import java.time.LocalDate;

@EqualsAndHashCode(exclude = {"createdAt", "none"} )
public class Employee {
    private String name;
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;
}

Include Fields

Nảy giờ chúng ta đã biết được cách loại bỏ các thuộc tính không cần thiết khi @EqualsAndHashCode generate. Phần này chúng ta sẽ tìm hiểu cách chỉ định các thuộc tính được @EqualsAndHashCode generate, các thuộc tính không được chỉ định sẽ bị loại bỏ.

Include Field Level

Sử dụng @EqualsAndHashCode.Include để chú thích các thuộc tính được generate, chú ý chúng ta phải chỉ gán giá trị onlyExplicitlyIncluded = true trong @EqualsAndHashCode.

import lombok.EqualsAndHashCode;

import java.time.LocalDate;

@EqualsAndHashCode(onlyExplicitlyIncluded = true )
public class Employee {
    @EqualsAndHashCode.Include
    private String name;
    @EqualsAndHashCode.Include
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;
}

Include Method Level

Nếu bạn chú thích @EqualsAndHashCode.Include cho 1 method, chúng cũng sẽ được sử dụng để generate equals()hashCode() method.

Ví dụ mình muốn so sánh các Employee theo mức lương hơn là chính xác từng số.

  • Nhỏ hơn 10tr: Level 1
  • 10tr – 15tr: Level 2
  • 15tr – 30tr: Level 3
  • 30tr trở lên: Level 4
import lombok.EqualsAndHashCode;
import lombok.Setter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@EqualsAndHashCode(onlyExplicitlyIncluded = true )
public class Employee {
    @EqualsAndHashCode.Include
    private String name;
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;

    @EqualsAndHashCode.Include
    public int canGo() {
        if (salary < 10000000 && salary > 0) {
            return 1;
        } else if (salary >= 10000000 && salary < 15000000) {
            return 2;
        } else if (salary >= 15000000 && salary <=30000000) {
            return 3;
        } else {
            return 4;
        }
    }

    public Employee(String name, int salary, LocalDate createdAt) {
        this.name = name;
        this.salary = salary;
        this.createdAt = createdAt;
    }
}


class Main {
    public static void main(String[] agrs) {
        Employee employee1 = new Employee("Hai", 10000000, LocalDate.now());
        Employee employee2 = new Employee("Hai", 14000000, LocalDate.now());
        Employee employee3 = new Employee("Hai", 300000000, LocalDate.now());


        boolean b1 = employee1.equals(employee2); // TRUE
        boolean b2 = employee1.equals(employee3); // FALSE
    }
}

Note: Incluce method thường được dùng để tùy biến phép so sánh trên các thuộc tính thay vì so sánh giá trị của chúng với nhau.

Include Class Level

@EqualsAndHashCode of cho phép chúng ta chỉ định các thuộc tính được sử dụng để generate trong equals()hashCode() method. Thuộc tính of nhận 1 mảng string chứa tên các thuộc tính genenate, các string tên không đúng sẽ bị bỏ qua.

import lombok.EqualsAndHashCode;

import java.time.LocalDate;

@EqualsAndHashCode(of = {"name", "salary"})
public class Employee {
    private String name;
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;

}

Thừa kế và Call Supper class

Nếu sử dụng @EqualsAndHashCode subclass thừa kế từ 1 supperclass, thì mọi thứ sẽ phức tạp hơn 1 xíu, vì ở supperclass cũng chứa các thuộc tính và nó cũng có thể đã override equals()hashCode() method, việc này thường được khuyến cáo là không nên sử dụng vì có thể vi phạm nguyên tắc trong triển khai equals()hashCode() method.

Default callSupper

Khi bạn chú thích @EqualsAndHashCode cho 1 class, mặc định thuộc tính callSupperfalse, đồng nghĩa với việc nó sẽ không gọi equals()hashCode() ở supper class khi generate.

Custom callSupper

Case 1: Supper class không override equals()hashCode() method và callSupper = falseequals()hashCode() sẽ chỉ sử dụng các thuộc tính trong subclass. 

import lombok.EqualsAndHashCode;

import java.time.LocalDate;
import java.util.Objects;

@EqualsAndHashCode( exclude = {"createdAt"})
public class Employee extends Citizen {
    private String name;
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;

    public Employee(String name, int salary, String department) {
        super(department);
        this.name = name;
        this.salary = salary;
    }
}


class Citizen {
    protected String department;

    public Citizen(String supper) {
        this.department = supper;
    }
}

class Main {
    public static void main(String[] agrs) {
        Employee employee = new Employee("Hai", 10, "dev");
        Employee employee1 = new Employee("Hai", 10, "dev");
        Employee employee2 = new Employee("Hai", 10, "test");
        boolean b1 = employee.equals(employee1); // TRUE
        boolean b2 = employee.equals(employee2); // TRUE

    }
}

Case 2: Supper class không override equals()hashCode() method và callSupper = trueequals()hashCode() sẽ gọi các phương thức tương ứng ở supper class. Việc này dẫn đến các instance của subclass sẽ luôn khác như, vì equals() và hashCode() của supper class chính là của Object class, so sánh theo địa chỉ object.

import lombok.EqualsAndHashCode;

import java.time.LocalDate;

@EqualsAndHashCode(callSuper = true, exclude = {"createdAt"})
public class Employee extends Citizen {
    private String name;
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;

    public Employee(String name, int salary, String department) {
        super(department);
        this.name = name;
        this.salary = salary;
    }
}


class Citizen {
    protected String department;

    public Citizen(String supper) {
        this.department = supper;
    }
}

class Main {
    public static void main(String[] agrs) {
        Employee employee = new Employee("Hai", 10, "dev");
        Employee employee1 = new Employee("Hai", 10, "dev");
        Employee employee2 = new Employee("Hai", 10, "test");
        boolean b1 = employee.equals(employee1); // FALSE
        boolean b2 = employee.equals(employee2); // FALSE
    }
}

Case 3: Supper class override equals() và hashCode() method, callSupper = true. Subclass sẽ sử dụng equals() và hashCode() của supper class để generate code. Lúc này các thuộc tính ở subclass và supper class đều được kiểm tra.

import lombok.EqualsAndHashCode;

import java.time.LocalDate;
import java.util.Objects;

@EqualsAndHashCode(callSuper = true, exclude = {"createdAt"})
public class Employee extends Citizen {
    private String name;
    private int salary;
    private LocalDate createdAt;
    private transient int transientVar = 10;
    private final int finalVar = 10;

    public Employee(String name, int salary, String department) {
        super(department);
        this.name = name;
        this.salary = salary;
    }
}


class Citizen {
    protected String department;

    public Citizen(String supper) {
        this.department = supper;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Citizen)) return false;
        Citizen citizen = (Citizen) o;
        return Objects.equals(department, citizen.department);
    }

    @Override
    public int hashCode() {

        return Objects.hash(department);
    }
}

class Main {
    public static void main(String[] agrs) {
        Employee employee = new Employee("Hai", 10, "dev");
        Employee employee1 = new Employee("Hai", 10, "dev");
        Employee employee2 = new Employee("Hai", 10, "test");
        boolean b1 = employee.equals(employee1); // TRUE
        boolean b2 = employee.equals(employee2); // FALSE

    }
}

Case 4: Supper class không override equals() và hashCode() method, callSupper = false, tương tự trường hợp 1.

Tóm lược

Như vậy là chúng ta đã tìm hiểu xong về @EqualsAndHashCode annotation, trông có vẽ dễ nhưng đi sâu thì có rất nhiều thứ khó nhai lắm phải không! Theo kinh nghiệm mình sử dụng thì mình chỉ thường dùng @EqualsAndHashCode tại class Level thôi. Đôi lúc làm task có yêu cầu nhóm theo 2 hay nhiều thuộc tính của một đối tượng trong Database, công việc đơn giản là tạo class chứa tất cả các thuộc tính đó, đặt @EqualsAndHashCode tiến hành nhóm đối tượng trả về một HashMap, thử không đặt @EqualsAndHashCode xem, mỗi key là 1 phần tử đấy =))).

Nguồn tham khảo

https://projectlombok.org/features/EqualsAndHashCode

http://www.javabyexamples.com/delombok-equalsandhashcode/

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x