<audio id="audio" title="08 | 判等问题：程序里如何确定你就是你？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f0/20/f04eaa79bec7e05e5f26e85a22403e20.mp3"></audio>

你好，我是朱晔。今天，我来和你聊聊程序里的判等问题。

你可能会说，判等不就是一行代码的事情吗，有什么好说的。但，这一行代码如果处理不当，不仅会出现Bug，还可能会引起内存泄露等问题。涉及判等的Bug，即使是使用==这种错误的判等方式，也不是所有时候都会出问题。所以类似的判等问题不太容易发现，可能会被隐藏很久。

今天，我就equals、compareTo和Java的数值缓存、字符串驻留等问题展开讨论，希望你可以理解其原理，彻底消除业务代码中的相关Bug。

## 注意equals和==的区别

在业务代码中，我们通常使用equals或== 进行判等操作。equals是方法而==是操作符，它们的使用是有区别的：

- 对基本类型，比如int、long，进行判等，只能使用==，比较的是直接值。因为基本类型的值就是其数值。
- 对引用类型，比如Integer、Long和String，进行判等，需要使用equals进行内容判等。因为引用类型的直接值是指针，使用==的话，比较的是指针，也就是两个对象在内存中的地址，即比较它们是不是同一个对象，而不是比较对象的内容。

这就引出了我们必须必须要知道的第一个结论：**比较值的内容，除了基本类型只能使用==外，其他类型都需要使用equals**。

在开篇我提到了，即使使用==对Integer或String进行判等，有些时候也能得到正确结果。这又是为什么呢？

我们用下面的测试用例深入研究下：

- 使用==对两个值为127的直接赋值的Integer对象判等；
- 使用==对两个值为128的直接赋值的Integer对象判等；
- 使用==对一个值为127的直接赋值的Integer和另一个通过new Integer声明的值为127的对象判等；
- 使用==对两个通过new Integer声明的值为127的对象判等；
- 使用==对一个值为128的直接赋值的Integer对象和另一个值为128的int基本类型判等。

```
Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
log.info(&quot;\nInteger a = 127;\n&quot; +
        &quot;Integer b = 127;\n&quot; +
        &quot;a == b ? {}&quot;,a == b);    // true

Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
log.info(&quot;\nInteger c = 128;\n&quot; +
        &quot;Integer d = 128;\n&quot; +
        &quot;c == d ? {}&quot;, c == d);   //false

Integer e = 127; //Integer.valueOf(127)
Integer f = new Integer(127); //new instance
log.info(&quot;\nInteger e = 127;\n&quot; +
        &quot;Integer f = new Integer(127);\n&quot; +
        &quot;e == f ? {}&quot;, e == f);   //false

Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
log.info(&quot;\nInteger g = new Integer(127);\n&quot; +
        &quot;Integer h = new Integer(127);\n&quot; +
        &quot;g == h ? {}&quot;, g == h);  //false

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

```

通过运行结果可以看到，虽然看起来永远是在对127和127、128和128判等，但==却没有永远给我们true的答复。原因是什么呢？

第一个案例中，编译器会把Integer a = 127转换为Integer.valueOf(127)。查看源码可以发现，这个**转换在内部其实做了缓存，使得两个Integer指向同一个对象**，所以==返回true。

```
public static Integer valueOf(int i) {
    if (i &gt;= IntegerCache.low &amp;&amp; i &lt;= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

```

第二个案例中，之所以同样的代码128就返回false的原因是，默认情况下会缓存[-128, 127]的数值，而128处于这个区间之外。设置JVM参数加上-XX:AutoBoxCacheMax=1000再试试，是不是就返回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(&quot;java.lang.Integer.IntegerCache.high&quot;);
        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 &lt; cache.length; k++)
            cache[k] = new Integer(j++);


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

```

第三和第四个案例中，New出来的Integer始终是不走缓存的新对象。比较两个新对象，或者比较一个新对象和一个来自缓存的对象，结果肯定不是相同的对象，因此返回false。

第五个案例中，我们把装箱的Integer和基本类型int比较，前者会先拆箱再比较，比较的肯定是数值而不是引用，因此返回true。

看到这里，对于Integer什么时候是相同对象什么时候是不同对象，就很清楚了吧。但知道这些其实意义不大，因为在大多数时候，我们并不关心Integer对象是否是同一个，**只需要记得比较Integer的值请使用equals，而不是==**（对于基本类型int的比较当然只能使用==）。

其实，我们应该都知道这个原则，只是有的时候特别容易忽略。以我之前遇到过的一个生产事故为例，有这么一个枚举定义了订单状态和对于状态的描述：

```
enum StatusEnum {
    CREATED(1000, &quot;已创建&quot;),
    PAID(1001, &quot;已支付&quot;),
    DELIVERED(1002, &quot;已送到&quot;),
    FINISHED(1003, &quot;已完成&quot;);

    private final Integer status; //注意这里的Integer
    private final String desc;

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

```

在业务代码中，开发同学使用了==对枚举和入参OrderQuery中的status属性进行判等：

```
@Data
public class OrderQuery {
    private Integer status;
    private String name;
}

@PostMapping(&quot;enumcompare&quot;)
public void enumcompare(@RequestBody OrderQuery orderQuery){
    StatusEnum statusEnum = StatusEnum.DELIVERED;
    log.info(&quot;orderQuery:{} statusEnum:{} result:{}&quot;, orderQuery, statusEnum, statusEnum.status == orderQuery.getStatus());
}

```

**因为枚举和入参OrderQuery中的status都是包装类型，所以通过==判等肯定是有问题的。只是这个问题比较隐晦**，究其原因在于：

- 只看枚举的定义CREATED(1000, “已创建”)，容易让人误解status值是基本类型；
- 因为有Integer缓存机制的存在，所以使用==判等并不是所有情况下都有问题。在这次事故中，订单状态的值从100开始增长，程序一开始不出问题，直到订单状态超过127后才出现Bug。

在了解清楚为什么Integer使用==判等有时候也有效的原因之后，我们再来看看为什么String也有这个问题。我们使用几个用例来测试下：

- 对两个直接声明的值都为1的String使用==判等；
- 对两个new出来的值都为2的String使用==判等；
- 对两个new出来的值都为3的String先进行intern操作，再使用==判等；
- 对两个new出来的值都为4的String通过equals判等。

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

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

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

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

```

在分析这个结果之前，我先和你说说Java的字符串常量池机制。首先要明确的是其设计初衷是节省内存。当代码中出现双引号形式创建字符串对象时，JVM会先对这个字符串进行检查，如果字符串常量池中存在相同内容的字符串对象的引用，则将这个引用返回；否则，创建新的字符串对象，然后将这个引用放入字符串常量池，并返回该引用。这种机制，就是字符串驻留或池化。

再回到刚才的例子，再来分析一下运行结果：

- 第一个案例返回true，因为Java的字符串驻留机制，直接使用双引号声明出来的两个String对象指向常量池中的相同字符串。
- 第二个案例，new出来的两个String是不同对象，引用当然不同，所以得到false的结果。
- 第三个案例，使用String提供的intern方法也会走常量池机制，所以同样能得到true。
- 第四个案例，通过equals对值内容判等，是正确的处理方式，当然会得到true。

**虽然使用new声明的字符串调用intern方法，也可以让字符串进行驻留，但在业务代码中滥用intern，可能会产生性能问题**。

写代码测试一下，通过循环把1到1000万之间的数字以字符串形式intern后，存入一个List：

```
List&lt;String&gt; list = new ArrayList&lt;&gt;();

@GetMapping(&quot;internperformance&quot;)
public int internperformance(@RequestParam(value = &quot;size&quot;, defaultValue = &quot;10000000&quot;)int size) {
    //-XX:+PrintStringTableStatistics
    //-XX:StringTableSize=10000000
    long begin = System.currentTimeMillis();
    list = IntStream.rangeClosed(1, size)
            .mapToObj(i-&gt; String.valueOf(i).intern())
            .collect(Collectors.toList());
    log.info(&quot;size:{} took:{}&quot;, size, System.currentTimeMillis() - begin);
    return list.size();
}

```

在启动程序时设置JVM参数-XX:+PrintStringTableStatistic，程序退出时可以打印出字符串常量表的统计信息。调用接口后关闭程序，输出如下：

```
[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

```

可以看到，1000万次intern操作耗时居然超过了44秒。

其实，原因在于字符串常量池是一个固定容量的Map。如果容量太小（Number of buckets=60013）、字符串太多（1000万个字符串），那么每一个桶中的字符串数量会非常多，所以搜索起来就很慢。输出结果中的Average bucket size=167，代表了Map中桶的平均长度是167。

解决方式是，设置JVM参数-XX:StringTableSize，指定更多的桶。设置-XX:StringTableSize=10000000后，重启应用：

```
[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

```

可以看到，1000万次调用耗时只有5.5秒，Average bucket size降到了1，效果明显。

好了，是时候给出第二原则了：**没事别轻易用intern，如果要用一定要注意控制驻留的字符串的数量，并留意常量表的各项指标**。

## 实现一个equals没有这么简单

如果看过Object类源码，你可能就知道，equals的实现其实是比较对象引用：

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

```

之所以Integer或String能通过equals实现内容判等，是因为它们都重写了这个方法。比如，String的equals的实现：

```
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;
}

```

对于自定义类型，如果不重写equals的话，默认就是使用Object基类的按引用的比较方式。我们写一个自定义类测试一下。

假设有这样一个描述点的类Point，有x、y和描述三个属性：

```
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;
    }
}

```

定义三个点p1、p2和p3，其中p1和p2的描述属性不同，p1和p3的三个属性完全相同，并写一段代码测试一下默认行为：

```
Point p1 = new Point(1, 2, &quot;a&quot;);
Point p2 = new Point(1, 2, &quot;b&quot;);
Point p3 = new Point(1, 2, &quot;a&quot;);
log.info(&quot;p1.equals(p2) ? {}&quot;, p1.equals(p2));
log.info(&quot;p1.equals(p3) ? {}&quot;, p1.equals(p3));

```

通过equals方法比较p1和p2、p1和p3均得到false，原因正如刚才所说，我们并没有为Point类实现自定义的equals方法，Object超类中的equals默认使用==判等，比较的是对象的引用。

我们期望的逻辑是，只要x和y这2个属性一致就代表是同一个点，所以写出了如下的改进代码，重写equals方法，把参数中的Object转换为Point比较其x和y属性：

```
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 &amp;&amp; y == that.y;
    }
}

```

为测试改进后的Point是否可以满足需求，我们定义了三个用例：

- 比较一个Point对象和null；
- 比较一个Object对象和一个Point对象；
- 比较两个x和y属性值相同的Point对象。

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

Object o = new Object();
try {
    log.info(&quot;p1.equals(expression) ? {}&quot;, p1.equals(o));
} catch (Exception ex) {
    log.error(ex.getMessage());
}

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

```

通过日志中的结果可以看到，第一次比较出现了空指针异常，第二次比较出现了类型转换异常，第三次比较符合预期输出了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

```

**通过这些失效的用例，我们大概可以总结出实现一个更好的equals应该注意的点：**

- 考虑到性能，可以先进行指针判等，如果对象是同一个那么直接返回true；
- 需要对另一方进行判空，空对象和自身进行比较，结果一定是fasle；
- 需要判断两个对象的类型，如果类型都不同，那么直接返回false；
- 确保类型相同的情况下再进行类型强制转换，然后逐一判断所有字段。

修复和改进后的equals方法如下：

```
@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 &amp;&amp; y == that.y;
}		

```

改进后的equals看起来完美了，但还没完。我们继续往下看。

## hashCode和equals要配对实现

我们来试试下面这个用例，定义两个x和y属性值完全一致的Point对象p1和p2，把p1加入HashSet，然后判断这个Set中是否存在p2：

```
PointWrong p1 = new PointWrong(1, 2, &quot;a&quot;);
PointWrong p2 = new PointWrong(1, 2, &quot;b&quot;);

HashSet&lt;PointWrong&gt; points = new HashSet&lt;&gt;();
points.add(p1);
log.info(&quot;points.contains(p2) ? {}&quot;, points.contains(p2));

```

按照改进后的equals方法，这2个对象可以认为是同一个，Set中已经存在了p1就应该包含p2，但结果却是false。

出现这个Bug的原因是，散列表需要使用hashCode来定位元素放到哪个桶。如果自定义对象没有实现自定义的hashCode方法，就会使用Object超类的默认实现，**得到的两个hashCode是不同的，导致无法满足需求**。

要自定义hashCode，我们可以直接使用Objects.hash方法来实现，改进后的Point类如下：

```
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);
    }
}

```

改进equals和hashCode后，再测试下之前的四个用例，结果全部符合预期。

```
[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

```

看到这里，你可能会觉得自己实现equals和hashCode很麻烦，实现equals有很多注意点而且代码量很大。不过，实现这两个方法也有简单的方式，一是后面要讲到的Lombok方法，二是使用IDE的代码生成功能。IDEA的类代码快捷生成菜单支持的功能如下：

<img src="https://static001.geekbang.org/resource/image/94/36/944fe3549e4c24936e9837d0bf1e3936.jpg" alt="">

## 注意compareTo和equals的逻辑一致性

除了自定义类型需要确保equals和hashCode要逻辑一致外，还有一个更容易被忽略的问题，即compareTo同样需要和equals确保逻辑一致性。

我之前遇到过这么一个问题，代码里本来使用了ArrayList的indexOf方法进行元素搜索，但是一位好心的开发同学觉得逐一比较的时间复杂度是O(n)，效率太低了，于是改为了排序后通过Collections.binarySearch方法进行搜索，实现了O(log n)的时间复杂度。没想到，这么一改却出现了Bug。

我们来重现下这个问题。首先，定义一个Student类，有id和name两个属性，并实现了一个Comparable接口来返回两个id的值：

```
@Data
@AllArgsConstructor
class Student implements Comparable&lt;Student&gt;{
    private int id;
    private String name;

    @Override
    public int compareTo(Student other) {
        int result = Integer.compare(other.id, id);
        if (result==0)
            log.info(&quot;this {} == other {}&quot;, this, other);
        return result;
    }
}

```

然后，写一段测试代码分别通过indexOf方法和Collections.binarySearch方法进行搜索。列表中我们存放了两个学生，第一个学生id是1叫zhang，第二个学生id是2叫wang，搜索这个列表是否存在一个id是2叫li的学生：

```
@GetMapping(&quot;wrong&quot;)
public void wrong(){

    List&lt;Student&gt; list = new ArrayList&lt;&gt;();
    list.add(new Student(1, &quot;zhang&quot;));
    list.add(new Student(2, &quot;wang&quot;));
    Student student = new Student(2, &quot;li&quot;);

    log.info(&quot;ArrayList.indexOf&quot;);
    int index1 = list.indexOf(student);
    Collections.sort(list);
    log.info(&quot;Collections.binarySearch&quot;);
    int index2 = Collections.binarySearch(list, student);

    log.info(&quot;index1 = &quot; + index1);
    log.info(&quot;index2 = &quot; + index2);
}

```

代码输出的日志如下：

```
[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

```

我们注意到如下几点：

- **binarySearch方法内部调用了元素的compareTo方法进行比较**；
- indexOf的结果没问题，列表中搜索不到id为2、name是li的学生；
- binarySearch返回了索引1，代表搜索到的结果是id为2，name是wang的学生。

修复方式很简单，确保compareTo的比较逻辑和equals的实现一致即可。重新实现一下Student类，通过Comparator.comparing这个便捷的方法来实现两个字段的比较：

```
@Data
@AllArgsConstructor
class StudentRight implements Comparable&lt;StudentRight&gt;{
    private int id;
    private String name;

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

```

其实，这个问题容易被忽略的原因在于两方面：

- 一是，我们使用了Lombok的@Data标记了Student，@Data注解（详见[这里](https://projectlombok.org/features/Data)）其实包含了@EqualsAndHashCode注解（详见[这里](https://projectlombok.org/features/EqualsAndHashCode)）的作用，也就是默认情况下使用类型所有的字段（不包括static和transient字段）参与到equals和hashCode方法的实现中。因为这两个方法的实现不是我们自己实现的，所以容易忽略其逻辑。
- 二是，compareTo方法需要返回数值，作为排序的依据，容易让人使用数值类型的字段随意实现。

我再强调下，**对于自定义的类型，如果要实现Comparable，请记得equals、hashCode、compareTo三者逻辑一致**。

## 小心Lombok生成代码的“坑”

Lombok的@Data注解会帮我们实现equals和hashcode方法，但是有继承关系时，Lombok自动生成的方法可能就不是我们期望的了。

我们先来研究一下其实现：定义一个Person类型，包含姓名和身份证两个字段：

```
@Data
class Person {
    private String name;
    private String identity;

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

```

对于身份证相同、姓名不同的两个Person对象：

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

```

使用equals判等会得到false。如果你希望只要身份证一致就认为是同一个人的话，可以使用@EqualsAndHashCode.Exclude注解来修饰name字段，从equals和hashCode的实现中排除name字段：

```
@EqualsAndHashCode.Exclude
private String name;

```

修改后得到true。打开编译后的代码可以看到，Lombok为Person生成的equals方法的实现，确实只包含了identity属性：

```
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;
        }
    }
}

```

但到这里还没完，如果类型之间有继承，Lombok会怎么处理子类的equals和hashCode呢？我们来测试一下，写一个Employee类继承Person，并新定义一个公司属性：

```
@Data
class Employee extends Person {

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

```

在如下的测试代码中，声明两个Employee实例，它们具有相同的公司名称，但姓名和身份证均不同：

```
Employee employee1 = new Employee(&quot;zhuye&quot;,&quot;001&quot;, &quot;bkjk.com&quot;);
Employee employee2 = new Employee(&quot;Joseph&quot;,&quot;002&quot;, &quot;bkjk.com&quot;);
log.info(&quot;employee1.equals(employee2) ? {}&quot;, employee1.equals(employee2));	

```

很遗憾，结果是true，显然是没有考虑父类的属性，而认为这两个员工是同一人，**说明@EqualsAndHashCode默认实现没有使用父类属性。**

为解决这个问题，我们可以手动设置callSuper开关为true，来覆盖这种默认行为：

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

```

修改后的代码，实现了同时以子类的属性company加上父类中的属性identity，作为equals和hashCode方法的实现条件（实现上其实是调用了父类的equals和hashCode）。

## 重点回顾

现在，我们来回顾下对象判等和比较的重点内容吧。

首先，我们要注意equals和== 的区别。业务代码中进行内容的比较，针对基本类型只能使用==，针对Integer、String在内的引用类型，需要使用equals。Integer和String的坑在于，使用==判等有时也能获得正确结果。

其次，对于自定义类型，如果类型需要参与判等，那么务必同时实现equals和hashCode方法，并确保逻辑一致。如果希望快速实现equals、hashCode方法，我们可以借助IDE的代码生成功能，或使用Lombok来生成。如果类型也要参与比较，那么compareTo方法的逻辑同样需要和equals、hashCode方法一致。

最后，Lombok的@EqualsAndHashCode注解实现equals和hashCode的时候，默认使用类型所有非static、非transient的字段，且不考虑父类。如果希望改变这种默认行为，可以使用@EqualsAndHashCode.Exclude排除一些字段，并设置callSuper = true来让子类的equals和hashCode调用父类的相应方法。

在比较枚举值和POJO参数值的例子中，我们还可以注意到，使用==来判断两个包装类型的低级错误，确实容易被忽略。所以，**我建议你在IDE中安装阿里巴巴的Java规约插件**（详见[这里](https://github.com/alibaba/p3c)），来及时提示我们这类低级错误：

<img src="https://static001.geekbang.org/resource/image/fe/c3/fe020d747a35cec23e5d92c1277d02c3.png" alt="">

今天用到的代码，我都放在了GitHub上，你可以点击[这个链接](https://github.com/JosephZhu1983/java-common-mistakes)查看。

## 思考与讨论

1. 在实现equals时，我是先通过getClass方法判断两个对象的类型，你可能会想到还可以使用instanceof来判断。你能说说这两种实现方式的区别吗？
1. 在第三节的例子中，我演示了可以通过HashSet的contains方法判断元素是否在HashSet中，同样是Set的TreeSet其contains方法和HashSet有什么区别吗？

有关对象判等、比较，你还遇到过其他坑吗？我是朱晔，欢迎在评论区与我留言分享你的想法，也欢迎你把这篇文章分享给你的朋友或同事，一起交流。
