08 Equality Issues How to Ensure You Are You in the Program

08 Equality Issues How to Ensure You Are You in the Program #

Today, I want to talk to you about the issue of equality in programs.

You might say, isn’t equality just a matter of a single line of code? What is there to discuss? However, if this line of code is not handled properly, it can not only cause bugs, but also lead to issues such as memory leaks. Bugs related to equality, even when using incorrect methods like ==, may not always manifest themselves. Therefore, such equality issues are not easy to detect and can remain hidden for a long time.

Today, I will discuss the topics of equals, compareTo, and Java’s number caching, string interning, etc. I hope that by understanding their principles, we can eliminate related bugs in our business code completely.

Pay attention to the difference between equals and == #

In business code, we usually use equals or == for equality comparisons. equals is a method while == is an operator. There are differences in how they are used:

For primitive types like int and long, we can only use == for equality comparison, as it compares the direct values of the primitive types. This is because the value of a primitive type is its numerical value.

For reference types like Integer, Long, and String, we need to use equals for equality comparisons, as the direct value of a reference type is a pointer. Using == compares the pointers, which are the addresses of the objects in memory, to check if they are the same object, rather than comparing the contents of the objects.

This leads us to the first conclusion that we must know: for comparing the contents of values, we need to use equals for all types except primitive types, where we can only use ==.

In the introduction, I mentioned that sometimes we can get the correct result even when using == for equality comparisons of Integer or String. Why is that?

Let’s dive deeper into this with the following test cases:

  1. Equality comparison using == for two Integer objects with values of 127 assigned directly.
  2. Equality comparison using == for two Integer objects with values of 128 assigned directly.
  3. Equality comparison using == for one Integer object with a value of 127 assigned directly and another Integer object with a value of 127 declared using new Integer.
  4. Equality comparison using == for two Integer objects with values of 127 declared using new Integer.
  5. Equality comparison using == for one Integer object with a value of 128 assigned directly and another int primitive type with a value of 128.
Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
log.info("\nInteger a = 127;\n" +
        "Integer b = 127;\n" +
        "a == b ? {}",a == b);    // true
Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
log.info("\nInteger c = 128;\n" +
        "Integer d = 128;\n" +
        "c == d ? {}", c == d);   //false
Integer e = 127; //Integer.valueOf(127)
Integer f = new Integer(127); //new instance
log.info("\nInteger e = 127;\n" +
        "Integer f = new Integer(127);\n" +
        "e == f ? {}", e == f);   //false
Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
log.info("\nInteger g = new Integer(127);\n" +
        "Integer h = new Integer(127);\n" +
        "g == h ? {}", g == h);  //false
Integer i = 128; //unbox
int j = 128;

log.info("\nInteger i = 128;\n" +
        "int j = 128;\n" +
        "i == j ? {}", i == j); //true

From the execution results, we can see that even though it always appears to be comparing 127 and 127, 128 and 128, == does not always give us a true answer. Why is that?

In the first case, the compiler will convert Integer a = 127 to Integer.valueOf(127). By looking at the source code, we can see that this conversion actually caches the value internally, making both Integer objects point to the same object, so == returns true.

public static Integer valueOf(int i) {

    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

In the second case, the reason why the same code returns false when 128 is used is that by default, the JVM caches values in the range [-128, 127], and 128 is outside this range. Try setting the JVM parameter -XX:AutoBoxCacheMax=1000 and see if it returns true.

private static class IntegerCache {

    static final int low = -128;
    static final int high;

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }
}

In the third and fourth cases, new creates new Integer objects that do not go through caching. Comparing two new objects or comparing a new object with an object from the cache will definitely not return the same object, hence it returns false.

In the fifth case, when we compare a boxed Integer with a primitive int, the former will be unboxed before comparison, so the comparison is made between values rather than references, hence it returns true.

Seeing this, it should be clear when an Integer is the same object and when it is a different object. However, knowing this has little significance because most of the time, we don’t care if Integer objects are the same, we just need to remember to use equals for comparing the values of Integer (and reference types in general), instead of using == (for primitive type int, we can only use ==).

In fact, we should already know this principle, but sometimes it is particularly easy to overlook. Let’s take a production incident I encountered as an example. There was an enum that defined order status and its corresponding descriptions:

enum StatusEnum {

    CREATED(1000, "已创建"),
    PAID(1001, "已支付"),
    DELIVERED(1002, "已送到"),
    FINISHED(1003, "已完成");

    private final Integer status; //pay attention to this Integer

    private final String desc;

    StatusEnum(Integer status, String desc) {
        this.status = status;
        this.desc = desc;
    }
}

In the business code, the developers used == to compare the enum with the status attribute in an input parameter OrderQuery:

@Data
public class OrderQuery {

    private Integer status;
    private String name;

}

@PostMapping("enumcompare")
public void enumcompare(@RequestBody OrderQuery orderQuery){

    StatusEnum statusEnum = StatusEnum.DELIVERED;

    log.info("orderQuery:{} statusEnum:{} result:{}", orderQuery, statusEnum, Objects.equals(orderQuery.getStatus(), statusEnum.getStatus()));

}

Because the enumeration and the parameter status in OrderQuery are both wrapper types, using == for comparison is definitely problematic. However, this problem is somewhat hidden, and the reason lies in the following:

Looking at the definition of the enumeration CREATED(1000, "已创建"), it is easy to misunderstand that the status value is a primitive type;

Because of the existence of integer caching mechanism, using == for comparison is not always problematic. In this incident, the value of the order status starts increasing from 100, and the program initially has no issues until the order status exceeds 127.

After understanding why using == for comparison with Integer can sometimes work, let’s take a look at why it also happens with String. Let’s test a few cases:

Comparing two directly declared String values of 1 using ==;

Comparing two String objects created through new with values of 2 using ==;

First interning two String objects created through new with values of 3, and then comparing using ==;

Comparing two String objects created through new with values of 4 using equals.

String a = "1";
String b = "1";
log.info("\nString a = \"1\";\n" +
        "String b = \"1\";\n" +
        "a == b ? {}", a == b); //true

String c = new String("2");
String d = new String("2");

log.info("\nString c = new String(\"2\");\n" +
        "String d = new String(\"2\");\n" +
        "c == d ? {}", c == d); //false

String e = new String("3").intern();
String f = new String("3").intern();
log.info("\nString e = new String(\"3\").intern();\n" +
        "String f = new String(\"3\").intern();\n" +
        "e == f ? {}", e == f); //true

String g = new String("4");
String h = new String("4");
log.info("\nString g = new String(\"4\");\n" +
        "String h = new String(\"4\");\n" +
        "g == h ? {}", g.equals(h)); //true

Before analyzing the results, I will first explain the Java string constant pool mechanism. First of all, it was designed to save memory. When there is a string object created in the form of double quotes in the code, the JVM will check this string. If a reference to an existing string object with the same content exists in the string constant pool, it will return this reference. Otherwise, it will create a new string object, put this reference into the string constant pool, and return it. This mechanism is referred to as string interning or pooling.

Now, let’s analyze the results of the previous cases:

The first case returns true because of Java’s string interning mechanism. Two string objects directly declared using double quotes both point to the same string in the constant pool.

In the second case, the two String objects created using new are different objects, so their references are different, resulting in a false result.

In the third case, using the intern() method provided by String also goes through the constant pool mechanism, and therefore it returns true.

In the fourth case, comparing the values using equals is the correct approach, so it returns true.

Although using new to declare a string and invoking the intern method can also make the string interned, abusing intern in business code may cause performance issues.

Let’s test it by creating a list and internning the strings from 1 to 10 million in a loop:

List<String> list = new ArrayList<>();

@GetMapping("internperformance")
public int internperformance(@RequestParam(value = "size", defaultValue = "10000000")int size) {

    //-XX:+PrintStringTableStatistics
    //-XX:StringTableSize=10000000
    long begin = System.currentTimeMillis();
    list = IntStream.rangeClosed(1, size)
            .mapToObj(i-> String.valueOf(i).intern())
            .collect(Collectors.toList());

    log.info("size:{} took:{}", size, System.currentTimeMillis() - begin);
    return list.size();
}

By setting the JVM parameter -XX:+PrintStringTableStatistic at startup, the program will print out statistics about the string constant table when the program exits. After calling the API and shutting down the program, the following is the output:

[11:01:57.770] [http-nio-45678-exec-2] [INFO ] [.t.c.e.d.IntAndStringEqualController:54  ] - size:10000000 took:44907
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :  10030230 = 240725520 bytes, avg  24.000
Number of literals      :  10030230 = 563005568 bytes, avg  56.131
Total footprint         :           = 804211192 bytes
Average bucket size     :   167.134
Variance of bucket size :    55.808
Std. dev. of bucket size:     7.471
Maximum bucket size     :       198

As we can see, interning 10 million times took more than 44 seconds.

The reason is that the string constant pool is a Map of fixed capacity. If the capacity is too small (Number of buckets=60013) and there are too many strings (10 million strings), then there will be a large number of strings in each bucket, making the search slow. The Average bucket size=167 in the output indicates that the average length of each bucket in the map is 167.

The solution is to set the JVM parameter -XX:StringTableSize to specify more buckets. After setting -XX:StringTableSize=10000000 and restarting the application:

[11:09:04.475] [http-nio-45678-exec-1] [INFO ] [.t.c.e.d.IntAndStringEqualController:54  ] - size:10000000 took:5557
StringTable statistics:
Number of buckets       :  10000000 =  80000000 bytes, avg   8.000
Number of entries       :  10030156 = 240723744 bytes, avg  24.000
Number of literals      :  10030156 = 562999472 bytes, avg  56.131
Total footprint         :           = 883723216 bytes
Average bucket size     :     1.003
Variance of bucket size :     1.587
Std. dev. of bucket size:     1.260
Maximum bucket size     :        10

As we can see, interning 10 million times now takes only 5.5 seconds, and the average bucket size has dropped to 1, which is a significant improvement.

Alright, it’s time to give the second principle: avoid using intern unnecessarily, and if you have to use it, make sure to control the number of interned strings and pay attention to the various metrics of the constant table.

Implementing equals is not that simple #

If you have seen the source code of the Object class, you may know that the implementation of equals actually compares object references:

public boolean equals(Object obj) {
    return (this == obj);
}

The reason why Integer or String can achieve content comparison with equals is because they have overridden this method. For example, the implementation of equals in String:

public boolean equals(Object anObject) {

    if (this == anObject) {
        return true;
    }

    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

For custom types, if you don’t override equals, the default behavior is to use the reference comparison method of the Object base class. Let’s write a custom class to test it.

Suppose we have a class Point that describes a point with x, y, and description properties:

class Point {

    private int x;
    private int y;
    private final String desc;

    public Point(int x, int y, String desc) {
        this.x = x;
        this.y = y;
        this.desc = desc;
    }
}

Define three points p1, p2, and p3, where p1 and p2 differ in the description property, and p1 and p3 have exactly the same three properties. Write a code snippet to test the default behavior:

Point p1 = new Point(1, 2, "a");
Point p2 = new Point(1, 2, "b");
Point p3 = new Point(1, 2, "a");

log.info("p1.equals(p2) ? {}", p1.equals(p2));
log.info("p1.equals(p3) ? {}", p1.equals(p3));

Using the equals method to compare p1 and p2, and p1 and p3 both result in false. The reason is as previously stated: we did not implement a custom equals method for the Point class, and the equals in the Object superclass uses the == operator to compare, which compares object references.

The logic we expect is that as long as the x and y properties are the same, it represents the same point. So we wrote the following improved code, overriding the equals method and converting the Object parameter to Point to compare their x and y properties:

class PointWrong {

    private int x;
    private int y;

    private final String desc;

    public PointWrong(int x, int y, String desc) {
        this.x = x;
        this.y = y;
        this.desc = desc;
    }

    @Override
    public boolean equals(Object o) {
        PointWrong that = (PointWrong) o;
        return x == that.x && y == that.y;
    }
}

To test whether the improved Point meets the requirements, we define three test cases:

Comparing a Point object to null;

Comparing an Object object to a Point object;

Comparing two Point objects with the same x and y property values.

PointWrong p1 = new PointWrong(1, 2, "a");
try {
    log.info("p1.equals(null) ? {}", p1.equals(null));
} catch (Exception ex) {
    log.error(ex.getMessage());
}

Object o = new Object();

try {
    log.info("p1.equals(expression) ? {}", p1.equals(o));
} catch (Exception ex) {
    log.error(ex.getMessage());
}

PointWrong p2 = new PointWrong(1, 2, "b");
log.info("p1.equals(p2) ? {}", p1.equals(p2));

From the results in the log, we can see that a NullPointerException occurred in the first comparison, a ClassCastException occurred in the second comparison, and the third comparison produced the expected output of true.

[17:54:39.120] [http-nio-45678-exec-1] [ERROR] [t.c.e.demo1.EqualityMethodController:32  ] - java.lang.NullPointerException
[17:54:39.120] [http-nio-45678-exec-1] [ERROR] [t.c.e.demo1.EqualityMethodController:39  ] - java.lang.ClassCastException: java.lang.Object cannot be cast to org.geekbang.time.commonmistakes.equals.demo1.EqualityMethodController$PointWrong
[17:54:39.120] [http-nio-45678-exec-1] [INFO ] [t.c.e.demo1.EqualityMethodController:43  ] - p1.equals(p2) ? true

From these invalid test cases, we can roughly summarize the points to pay attention to when implementing a better equals:

Considering performance, perform reference comparison first, if the objects are the same, return true directly;

Need to check for null on the other side, comparing a null object and itself, the result must be false;

Need to check the types of the two objects, if the types are different, return false directly;

Ensure that the objects have the same type before performing type casting, and then compare all fields one by one.

The fixed and improved equals method is as follows:

@Override
public boolean equals(Object o) {

    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    PointRight that = (PointRight) o;
    return x == that.x && y == that.y;
}

The improved equals method looks perfect, but it’s not over yet. Let’s keep reading.

hashCode and equals should be implemented together #

Let’s try the following test case. Define two Point objects, p1 and p2, with exactly the same x and y values. Add p1 to a HashSet, and then check if p2 exists in this Set:

PointWrong p1 = new PointWrong(1, 2, "a");
PointWrong p2 = new PointWrong(1, 2, "b");

HashSet<PointWrong> points = new HashSet<>();
points.add(p1);
log.info("points.contains(p2) ? {}", points.contains(p2));

According to the improved equals method, these two objects should be considered the same, so if p1 is already in the Set, it should contain p2. However, the result is false.

The reason for this bug is that a hash table uses hashCode to locate which bucket an object should be placed in. If a custom object does not implement its own hashCode method, the default implementation from the Object superclass will be used, resulting in different hash codes for the two objects and failing to meet the requirement.

To customize hashCode, we can directly use the Objects.hash method to implement it. The improved Point class is as follows:

class PointRight {

    private final int x;
    private final int y;
    private final String desc;

    ...

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

After improving equals and hashCode, let’s test the previous four test cases again, and all the results meet expectations:

[18:25:23.091] [http-nio-45678-exec-4] [INFO ] [t.c.e.demo1.EqualityMethodController:54  ] - p1.equals(null) ? false
[18:25:23.093] [http-nio-45678-exec-4] [INFO ] [t.c.e.demo1.EqualityMethodController:61  ] - p1.equals(expression) ? false
[18:25:23.094] [http-nio-45678-exec-4] [INFO ] [t.c.e.demo1.EqualityMethodController:67  ] - p1.equals(p2) ? true
[18:25:23.094] [http-nio-45678-exec-4] [INFO ] [t.c.e.demo1.EqualityMethodController:71  ] - points.contains(p2) ? true

At this point, you might find implementing equals and hashCode yourself to be cumbersome. There are, however, simpler ways to implement these methods. One is through the use of Lombok, which will be discussed later, and the other is by utilizing the code generation feature in your IDE. In IntelliJ IDEA, the supported features in the code generation menu are as follows:

img

Pay Attention to the Consistency between compareTo and equals #

In addition to ensuring that equals and hashCode are logically consistent for custom types, there is another easily overlooked issue, which is the need to ensure the consistency between compareTo and equals.

I encountered a problem before where the code originally used the indexOf method of ArrayList for element searching. However, a well-intentioned developer colleague thought that the time complexity of comparing one by one was O(n), which was too low in efficiency. So he changed it to sort the list and then search using the Collections.binarySearch method, achieving a time complexity of O(log n). Unexpectedly, after this change, a bug occurred.

Let’s reproduce this problem. First, define a Student class with two attributes, id and name, and implement a Comparable interface to return the values of the two id:

@Data
@AllArgsConstructor
class Student implements Comparable<Student>{

    private int id;
    private String name;
    
    @Override
    public int compareTo(Student other) {

        int result = Integer.compare(other.id, id);
        if (result==0)
            log.info("this {} == other {}", this, other);
        return result;
    }
}

Then, write a test code to search the list using both the indexOf method and the Collections.binarySearch method. In the list, we store two students, the first student has an id of 1 and is named Zhang, and the second student has an id of 2 and is named Wang. We want to search this list for a student with an id of 2 and a name of Li:

@GetMapping("wrong")
public void wrong(){

    List<Student> list = new ArrayList<>();
    list.add(new Student(1, "zhang"));
    list.add(new Student(2, "wang"));
    
    Student student = new Student(2, "li");
    log.info("ArrayList.indexOf");
    
    int index1 = list.indexOf(student);
    Collections.sort(list);
    log.info("Collections.binarySearch");
    
    int index2 = Collections.binarySearch(list, student);
    log.info("index1 = " + index1);
    log.info("index2 = " + index2);
}

The log output of the code is as follows:

[18:46:50.226] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:28  ] - ArrayList.indexOf
[18:46:50.226] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:31  ] - Collections.binarySearch
[18:46:50.227] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:67  ] - this CompareToController.Student(id=2, name=wang) == other CompareToController.Student(id=2, name=li)
[18:46:50.227] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:34  ] - index1 = -1
[18:46:50.227] [http-nio-45678-exec-1] [INFO ] [t.c.equals.demo2.CompareToController:35  ] - index2 = 1

We notice the following points:

  • The binarySearch method calls the compareTo method of the elements for comparison internally.
  • The result of indexOf is correct, as the list cannot find a student with an id of 2 and a name of Li.
  • binarySearch returns index 1, indicating that the student with an id of 2 and a name of Wang is found.

The fix is simple. Make sure that the comparison logic in compareTo is consistent with the implementation of equals. Let’s redefine the Student class, using the convenient method Comparator.comparing to compare the two fields:

@Data
@AllArgsConstructor
class StudentRight implements Comparable<StudentRight>{

    private int id;
    private String name;

    @Override
    public int compareTo(StudentRight other) {
        return Comparator.comparing(StudentRight::getName)
                .thenComparingInt(StudentRight::getId)
                .compare(this, other);
    }
}

In fact, the reason why this problem is easily overlooked is twofold:

First, we used the @Data annotation of Lombok to mark the Student class. The @Data annotation actually includes the effect of the @EqualsAndHashCode annotation, which means that by default, all fields of the type (excluding static and transient fields) are involved in the implementation of equals and hashCode. Since these two methods are not implemented by ourselves, their logic is easily overlooked.

Second, the compareTo method needs to return a numerical value as the basis for sorting, which easily leads to arbitrary implementation using numerical fields.

Let me emphasize again that for custom types, if you want to implement Comparable, please remember to ensure the consistency of equals, hashCode, and compareTo.

Beware of the Pitfalls of Code Generation by Lombok #

The @Data annotation in Lombok helps us implement the equals and hashCode methods, but when there is inheritance involved, the methods generated by Lombok may not be what we expect.

Let’s first examine the implementation. We define a Person class that contains two fields: name and identity.

@Data
class Person {

    private String name;
    private String identity;

    public Person(String name, String identity) {
        this.name = name;
        this.identity = identity;
    }
}

For two Person objects with the same identity card but different names:

Person person1 = new Person("zhuye", "001");
Person person2 = new Person("Joseph", "001");
log.info("person1.equals(person2) ? {}", person1.equals(person2));

Using equals to compare them will yield false. If you want to consider them as the same person as long as their identity cards are the same, you can use the @EqualsAndHashCode.Exclude annotation to exclude the name field from the implementation of equals and hashCode:

@EqualsAndHashCode.Exclude
private String name;

After making this modification, the result will be true. If we examine the generated code after compilation, we can see that the implementation of the equals method generated by Lombok for Person indeed only includes the identity property:

public boolean equals(final Object o) {

    if (o == this) {
        return true;
    } else if (!(o instanceof LombokEquealsController.Person)) {
        return false;
    } else {
        LombokEquealsController.Person other = (LombokEquealsController.Person)o;
        if (!other.canEqual(this)) {
            return false;
        } else {
            Object this$identity = this.getIdentity();
            Object other$identity = other.getIdentity();
            if (this$identity == null) {
                if (other$identity != null) {
                    return false;
                }
            } else if (!this$identity.equals(other$identity)) {
                return false;
            }
            return true;
        }
    }
}

But we’re not done yet. What happens if there is inheritance between types and how does Lombok handle the equals and hashCode methods of the subclasses? Let’s test it out by writing an Employee class that extends Person and adds a new property for the company:

@Data
class Employee extends Person {

    private String company;

    public Employee(String name, String identity, String company) {
        super(name, identity);
        this.company = company;
    }
}

In the following test code, we declare two instances of Employee with the same company name but different names and identity cards:

Employee employee1 = new Employee("zhuye", "001", "bkjk.com");
Employee employee2 = new Employee("Joseph", "002", "bkjk.com");

log.info("employee1.equals(employee2) ? {}", employee1.equals(employee2));

Unfortunately, the result is true, indicating that the parent class’s properties were not taken into account, and it considers these two employees as the same person. This means that the default implementation of @EqualsAndHashCode did not consider the parent class properties.

To solve this issue, we can manually set the callSuper switch to true to override this default behavior:

@Data
@EqualsAndHashCode(callSuper = true)
class Employee extends Person {

With this modification, the code now implements equals and hashCode methods by considering both the child class property “company” and the parent class property “identity” as the conditions (in practice, it actually invokes the parent class’s equals and hashCode methods).

Key Review #

Now, let’s review the key points of object equality and comparison.

First, we need to pay attention to the difference between equals and ==. In business code, when comparing the content, == can only be used for primitive types, while for reference types such as Integer and String, we need to use equals. The pitfall with Integer and String is that sometimes == can also yield correct results.

Second, for custom types, if the type needs to be involved in equality testing, it is necessary to implement both equals and hashCode methods and ensure the logic is consistent. If we want to quickly implement equals and hashCode methods, we can use the code generation function of an IDE or use Lombok. If the type also needs to be compared, the logic of the compareTo method also needs to be consistent with the equals and hashCode methods.

Finally, Lombok’s @EqualsAndHashCode annotation implements equals and hashCode methods by default using all non-static, non-transient fields of the type, without considering the parent class. If we want to change this default behavior, we can use @EqualsAndHashCode.Exclude to exclude some fields and set callSuper = true to let the child class’s equals and hashCode methods call the corresponding methods of the parent class.

In the examples of comparing enum values and POJO parameter values, we can also notice that it is easy to overlook the common mistake of using == to compare two wrapper types. Therefore, I suggest installing the Alibaba Java coding guidelines plugin in your IDE (see here) to prompt us for such common mistakes in a timely manner:

img

I have put the code we used today on GitHub, you can click on this link to view it.

Reflection and Discussion #

When implementing equals, I first use the getClass method to determine the types of two objects. You may think of using instanceof to determine the types as well. Can you explain the difference between these two implementation methods?

In the example of Section 3, I demonstrated that you can use the contains method of a HashSet to determine if an element exists in the HashSet. As a TreeSet is also a type of Set, is there any difference between the contains method of a TreeSet and that of a HashSet?

Regarding object equality and comparison, have you encountered any other issues? I am Zhu Ye. Feel free to leave a comment in the comment section to share your thoughts, and you’re welcome to share this article with your friends or colleagues to facilitate discussion.