深入聊聊java中判等问题:你真的会用==和equals吗

59 阅读6分钟

文章目录

写在前面

判等问题是日常开发中遇到的最常见的问题之一,虽然简单但是其中蕴含着很多坑与技巧。

今天咱们就一起聊聊,判等问题。

equals 和 == 的区别

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

也就是说,比较值的内容,除了基本类型只能使用 == 外,其他类型都需要使用 equals。

警惕int类型数据的==比较

我们看以下代码:

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

对String类型的数据进行==比较


// 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,如果要用一定要注意控制驻留的字符串的数量,并留意常量表的各项指标。

实现一个equals

Object类的equals其实就是比较对象的地址值(用= =比较)。

String和Integer的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

对于自定义类型,如果不重写 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;
	}    
}

hashCode 和 equals 要配对实现

定义两个 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 是可以自动生成这俩方法的:
在这里插入图片描述

注意 compareTo 和 equals 的逻辑一致性

定义一个 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);
    }
}

注意contains方法、putIfAbsent、distinct等等

所有的集合类,判断对象是否相等,基本就是靠着equals或者hashcode或者compareTo,注意具体情况具体分析。

小心 Lombok 生成代码的“坑”

Lombok 的 @Data 注解会帮我们实现 equals 和 hashcode 方法,但是有继承关系时,Lombok 自动生成的方法可能就不是我们期望的了。
@EqualsAndHashCode 默认实现没有使用父类属性。
为解决这个问题,我们可以手动设置 callSuper 开关为 true,来覆盖这种默认行为:


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

参考资料

本文参考了朱晔(贝壳金融资深架构师)老师对于判等问题的讲解。