判等问题是日常开发中遇到的最常见的问题之一,虽然简单但是其中蕴含着很多坑与技巧。
今天咱们就一起聊聊,判等问题。
也就是说,比较值的内容,除了基本类型只能使用 == 外,其他类型都需要使用 equals。
我们看以下代码:
Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
log.info(a == b); // true
Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
log.info(c == d); //false
Integer e = 127; //Integer.valueOf(127)
Integer f = new Integer(127); //new instance
log.info(e == f); //false
// new的对象比较的是地址
Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
log.info(g == h); //false
// 把装箱的 Integer 和基本类型 int 比较,前者会先拆箱再比较,比较的肯定是数值而不是引用
Integer i = 128; //unbox
int j = 128;
log.info(i == j); //true
查看Integer的源码可以发现,这个转换在内部其实做了缓存,使得两个 Integer 指向同一个对象,所以 == 返回 true。
Integer中有个内部类IntegerCache,缓存-128到127的int类型的数据。
// Integer源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
// Integer内部类
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
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;
}
private IntegerCache() {}
}
其中,使用-XX:AutoBoxCacheMax=1000启动参数,可以将Integer的缓存扩展到1000
// Java 的字符串驻留机制,直接使用双引号声明出来的两个 String 对象指向常量池中的相同字符串
String a = "1";
String b = "1";
log.info(a == b); //true
// new新的对象用==比较为地址值的比较
String c = new String("2");
String d = new String("2");
log.info(c == d); //false
// intern()方法会将字符串存放在字符串常量池中
String e = new String("3").intern();
String f = new String("3").intern();
log.info(e == f); //true
// equals相当于调用了String的equals方法
String g = new String("4");
String h = new String("4");
log.info(g.equals(h)); //true
关于intern的详细用法请移步:String的Intern()方法,详解字符串常量池!
没事别轻易用 intern,如果要用一定要注意控制驻留的字符串的数量,并留意常量表的各项指标。
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;
}
Integer的equals的实现:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
对于自定义类型,如果不重写 equals 的话,默认就是使用 Object 基类的按引用的比较方式(Object 超类中的 equals 默认使用 == 判等,比较的是对象的引用)。
考虑到性能,可以先进行指针判等,如果对象是同一个那么直接返回 true;
需要对另一方进行判空,空对象和自身进行比较,结果一定是 fasle;
需要判断两个对象的类型,如果类型都不同,那么直接返回 false;
确保类型相同的情况下再进行类型强制转换,然后逐一判断所有字段。
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;
}
// 有问题的equals
@Override
public boolean equals(Object o) {
PointWrong that = (PointWrong) o;
return x == that.x && y == that.y;
}
// 改进后的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 && y == that.y;
}
}
定义两个 x 和 y 属性值完全一致的 Point 对象 p1 和 p2,把 p1 加入 HashSet,然后判断这个 Set 中是否存在 p2:
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)); // false
按照改进后的 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方法,IDEA 是可以自动生成这俩方法的:

定义一个 Student 类,有 id 和 name 两个属性,并实现了一个 Comparable 接口来返回两个 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;
}
}
然后,写一段测试代码分别通过 indexOf 方法和 Collections.binarySearch 方法进行搜索。
列表中我们存放了两个学生,第一个学生 id 是 1 叫 zhang,第二个学生 id 是 2 叫 wang,搜索这个列表是否存在一个 id 是 2 叫 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);
}
// 执行结果
[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 的学生。
我们看一下ArrayList的indexOf方法,实际上调用的是equals方法判断相等:
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
而Collections.binarySearch实际上是调用的compareTo方法判断相等:
private static <T>
int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key)
{
int low = 0;
int high = list.size()-1;
ListIterator<? extends Comparable<? super T>> i = list.listIterator();
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = get(i, mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
修复方式很简单,确保 compareTo 的比较逻辑和 equals 的实现一致即可。重新实现一下 Student 类,通过 Comparator.comparing 这个便捷的方法来实现两个字段的比较:
@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);
}
}
所有的集合类,判断对象是否相等,基本就是靠着equals或者hashcode或者compareTo,注意具体情况具体分析。
Lombok 的 @Data 注解会帮我们实现 equals 和 hashcode 方法,但是有继承关系时,Lombok 自动生成的方法可能就不是我们期望的了。
@EqualsAndHashCode 默认实现没有使用父类属性。
为解决这个问题,我们可以手动设置 callSuper 开关为 true,来覆盖这种默认行为:
@Data
@EqualsAndHashCode(callSuper = true)
class Employee extends Person {
本文参考了朱晔(贝壳金融资深架构师)老师对于判等问题的讲解。